From 600715670637b9597808dd20495cbb56b1565e9e Mon Sep 17 00:00:00 2001 From: Kevin Chen <140976422+wandb-kc@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:24:23 -0600 Subject: [PATCH 01/49] chore: Upgrade go to 1.25.3 (#104) * chore: Upgrade go to 1.25.3 * chore: Upgrade go to 1.25.3 --- .github/workflows/release.yaml | 2 +- .github/workflows/run-tests.yaml | 6 +- Dockerfile | 2 +- go.mod | 154 +++++------ go.sum | 440 ++++++++++++++----------------- 5 files changed, 274 insertions(+), 330 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index fd6f9c86..42b355b8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -51,7 +51,7 @@ jobs: - uses: actions/setup-go@v4 with: - go-version: 1.24 + go-version: 1.25 - name: Install Ginkgo run: go install github.com/onsi/ginkgo/v2/ginkgo@latest diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index c7a58799..7c4f7938 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -15,7 +15,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.24 + go-version: 1.25 - name: Install dependencies run: go mod download - name: Install Ginkgo @@ -31,7 +31,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.24 + go-version: 1.25 - name: Build run: make build @@ -43,7 +43,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: 1.24 + go-version: 1.25 - name: Install dependencies run: go mod download - name: Check for changes in go.mod or go.sum diff --git a/Dockerfile b/Dockerfile index 0905ba2f..67f0d3bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.24 AS manager-builder +FROM golang:1.25 AS manager-builder ARG TARGETOS ARG TARGETARCH diff --git a/go.mod b/go.mod index d7214d64..9477c3c2 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/wandb/operator -go 1.24.0 +go 1.25.3 -godebug default=go1.24 +godebug default=go1.25 require ( github.com/go-logr/logr v1.4.2 @@ -11,22 +11,21 @@ require ( github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/gomega v1.36.1 github.com/pkg/errors v0.9.1 - helm.sh/helm/v3 v3.17.4 - k8s.io/api v0.32.2 - k8s.io/apimachinery v0.32.2 - k8s.io/client-go v0.32.2 + helm.sh/helm/v3 v3.19.0 + k8s.io/api v0.34.0 + k8s.io/apimachinery v0.34.0 + k8s.io/client-go v0.34.0 sigs.k8s.io/controller-runtime v0.20.0 ) require ( - cel.dev/expr v0.18.0 // indirect + cel.dev/expr v0.24.0 // indirect dario.cat/mergo v1.0.1 // indirect - github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect - github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect - github.com/BurntSushi/toml v1.4.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/BurntSushi/toml v1.5.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect @@ -36,27 +35,20 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/containerd/containerd v1.7.24 // indirect + github.com/containerd/containerd v1.7.28 // indirect github.com/containerd/errdefs v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cyphar/filepath-securejoin v0.3.6 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v25.0.1+incompatible // indirect - github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v28.1.1+incompatible // indirect - github.com/docker/docker-credential-helpers v0.7.0 // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-metrics v0.0.1 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/evanphx/json-patch v5.9.0+incompatible // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fatih/color v1.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect @@ -70,20 +62,16 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.22.0 // indirect - github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect + github.com/google/cel-go v0.26.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/mux v1.8.0 // indirect - github.com/gorilla/websocket v1.5.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect @@ -91,7 +79,7 @@ require ( github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -104,76 +92,78 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/locker v1.0.1 // indirect github.com/moby/spdystream v0.5.0 // indirect - github.com/moby/term v0.5.0 // indirect + github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect - github.com/prometheus/client_golang v1.19.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/rubenv/sql-migrate v1.7.1 // indirect + github.com/rubenv/sql-migrate v1.8.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/pflag v1.0.9 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/sdk v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.36.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.43.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/mod v0.23.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.10.0 // indirect - golang.org/x/tools v0.30.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.37.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect - google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.35.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/grpc v1.72.1 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.32.2 // indirect - k8s.io/apiserver v0.32.2 // indirect - k8s.io/cli-runtime v0.32.2 // indirect - k8s.io/component-base v0.32.2 // indirect + k8s.io/apiextensions-apiserver v0.34.0 // indirect + k8s.io/apiserver v0.34.0 // indirect + k8s.io/cli-runtime v0.34.0 // indirect + k8s.io/component-base v0.34.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect - k8s.io/kubectl v0.32.2 // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - oras.land/oras-go v1.2.5 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/kustomize/api v0.18.0 // indirect - sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/kubectl v0.34.0 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + oras.land/oras-go/v2 v2.6.0 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index b6244c47..f97d0e80 100644 --- a/go.sum +++ b/go.sum @@ -1,107 +1,83 @@ -cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= -cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= -github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= -github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= -github.com/containerd/containerd v1.7.24 h1:zxszGrGjrra1yYJW/6rhm9cJ1ZQ8rkKBR48brqsa7nA= -github.com/containerd/containerd v1.7.24/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw= -github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= -github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/containerd v1.7.28 h1:Nsgm1AtcmEh4AHAJ4gGlNSaKgXiNccU270Dnf81FQ3c= +github.com/containerd/containerd v1.7.28/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= -github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc= -github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v25.0.1+incompatible h1:mFpqnrS6Hsm3v1k7Wa/BO23oz0k121MTbTO1lpcGSkU= -github.com/docker/cli v25.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= -github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= -github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= -github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= @@ -114,19 +90,16 @@ github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7Dlme github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -152,61 +125,50 @@ github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= -github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g= -github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= -github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -215,17 +177,12 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -233,6 +190,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= @@ -257,7 +216,6 @@ github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/Qd github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 h1:yVCLo4+ACVroOEr4iFU1iH46Ldlzz2rTuu18Ra7M8sU= github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2/go.mod h1:VzB2VoMh1Y32/QqDfg9ZJYHj99oM4LiGtqPZydTiQSQ= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= @@ -268,28 +226,20 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= -github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= -github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= @@ -298,13 +248,12 @@ github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -312,49 +261,45 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= -github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= +github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -366,85 +311,96 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= +go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= +go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -452,37 +408,36 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw= -google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -490,51 +445,50 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -helm.sh/helm/v3 v3.17.4 h1:GK+vgn9gKCyoH44+f3B5zpA78iH3AK4ywIInDEmmn/g= -helm.sh/helm/v3 v3.17.4/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= -k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= -k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= -k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= -k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= -k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= -k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.2 h1:WzyxAu4mvLkQxwD9hGa4ZfExo3yZZaYzoYvvVDlM6vw= -k8s.io/apiserver v0.32.2/go.mod h1:PEwREHiHNU2oFdte7BjzA1ZyjWjuckORLIK/wLV5goM= -k8s.io/cli-runtime v0.32.2 h1:aKQR4foh9qeyckKRkNXUccP9moxzffyndZAvr+IXMks= -k8s.io/cli-runtime v0.32.2/go.mod h1:a/JpeMztz3xDa7GCyyShcwe55p8pbcCVQxvqZnIwXN8= -k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= -k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= -k8s.io/component-base v0.32.2 h1:1aUL5Vdmu7qNo4ZsE+569PV5zFatM9hl+lb3dEea2zU= -k8s.io/component-base v0.32.2/go.mod h1:PXJ61Vx9Lg+P5mS8TLd7bCIr+eMJRQTyXe8KvkrvJq0= +helm.sh/helm/v3 v3.19.0 h1:krVyCGa8fa/wzTZgqw0DUiXuRT5BPdeqE/sQXujQ22k= +helm.sh/helm/v3 v3.19.0/go.mod h1:Lk/SfzN0w3a3C3o+TdAKrLwJ0wcZ//t1/SDXAvfgDdc= +k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= +k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= +k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= +k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= +k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= +k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg= +k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ= +k8s.io/cli-runtime v0.34.0 h1:N2/rUlJg6TMEBgtQ3SDRJwa8XyKUizwjlOknT1mB2Cw= +k8s.io/cli-runtime v0.34.0/go.mod h1:t/skRecS73Piv+J+FmWIQA2N2/rDjdYSQzEE67LUUs8= +k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= +k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= +k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8= +k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= -k8s.io/kubectl v0.32.2 h1:TAkag6+XfSBgkqK9I7ZvwtF0WVtUAvK8ZqTt+5zi1Us= -k8s.io/kubectl v0.32.2/go.mod h1:+h/NQFSPxiDZYX/WZaWw9fwYezGLISP0ud8nQKg+3g8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= -oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/kubectl v0.34.0 h1:NcXz4TPTaUwhiX4LU+6r6udrlm0NsVnSkP3R9t0dmxs= +k8s.io/kubectl v0.34.0/go.mod h1:bmd0W5i+HuG7/p5sqicr0Li0rR2iIhXL0oUyLF3OjR4= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.20.0 h1:jjkMo29xEXH+02Md9qaVXfEIaMESSpy3TBWPrsfQkQs= sigs.k8s.io/controller-runtime v0.20.0/go.mod h1:BrP3w158MwvB3ZbNpaAcIKkHQ7YGpYnzpoSTZ8E14WU= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= -sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= -sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= -sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= +sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= +sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From e70f07f504fd0ff166d644aabd0d08a23217f4b1 Mon Sep 17 00:00:00 2001 From: Kevin Chen <140976422+wandb-kc@users.noreply.github.com> Date: Tue, 25 Nov 2025 16:22:35 -0600 Subject: [PATCH 02/49] chore: Upgrade helm to 3.19.2 (#105) --- go.mod | 8 ++++---- go.sum | 15 ++++++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 9477c3c2..09004345 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/gomega v1.36.1 github.com/pkg/errors v0.9.1 - helm.sh/helm/v3 v3.19.0 + helm.sh/helm/v3 v3.19.2 k8s.io/api v0.34.0 k8s.io/apimachinery v0.34.0 k8s.io/client-go v0.34.0 @@ -35,11 +35,11 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/containerd/containerd v1.7.28 // indirect + github.com/containerd/containerd v1.7.29 // indirect github.com/containerd/errdefs v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect - github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/cyphar/filepath-securejoin v0.6.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect @@ -114,7 +114,7 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cobra v1.10.1 // indirect - github.com/spf13/pflag v1.0.9 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect diff --git a/go.sum b/go.sum index f97d0e80..f58caa84 100644 --- a/go.sum +++ b/go.sum @@ -40,8 +40,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/containerd/containerd v1.7.28 h1:Nsgm1AtcmEh4AHAJ4gGlNSaKgXiNccU270Dnf81FQ3c= -github.com/containerd/containerd v1.7.28/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= +github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE= +github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= @@ -54,8 +54,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= -github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= +github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -295,8 +295,9 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -450,8 +451,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -helm.sh/helm/v3 v3.19.0 h1:krVyCGa8fa/wzTZgqw0DUiXuRT5BPdeqE/sQXujQ22k= -helm.sh/helm/v3 v3.19.0/go.mod h1:Lk/SfzN0w3a3C3o+TdAKrLwJ0wcZ//t1/SDXAvfgDdc= +helm.sh/helm/v3 v3.19.2 h1:psQjaM8aIWrSVEly6PgYtLu/y6MRSmok4ERiGhZmtUY= +helm.sh/helm/v3 v3.19.2/go.mod h1:gX10tB5ErM+8fr7bglUUS/UfTOO8UUTYWIBH1IYNnpE= k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= From aa4ca21a9c32f84501fd1794ba8e851328a42444 Mon Sep 17 00:00:00 2001 From: Kevin Chen <140976422+wandb-kc@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:46:29 -0800 Subject: [PATCH 03/49] fix: Upgrade helm to 3.19.2 (#106) From 62ad6a4719c03f170c51bb617db6c43a8475deba Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 4 Dec 2025 21:47:05 +0000 Subject: [PATCH 04/49] chore(release): version 1.21.3 [skip ci] ### [1.21.3](https://github.com/wandb/operator/compare/v1.21.2...v1.21.3) (2025-12-04) ### Bug Fixes * Upgrade helm to 3.19.2 ([#106](https://github.com/wandb/operator/issues/106)) ([aa4ca21](https://github.com/wandb/operator/commit/aa4ca21a9c32f84501fd1794ba8e851328a42444)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62cbf507..4d076425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +### [1.21.3](https://github.com/wandb/operator/compare/v1.21.2...v1.21.3) (2025-12-04) + + +### Bug Fixes + +* Upgrade helm to 3.19.2 ([#106](https://github.com/wandb/operator/issues/106)) ([aa4ca21](https://github.com/wandb/operator/commit/aa4ca21a9c32f84501fd1794ba8e851328a42444)) + ### [1.21.2](https://github.com/wandb/operator/compare/v1.21.1...v1.21.2) (2025-10-02) From 802d886a0f5ea3dc46c1cc0316db0529440f5b6a Mon Sep 17 00:00:00 2001 From: Daniel Panzella Date: Mon, 9 Feb 2026 18:40:56 -0800 Subject: [PATCH 05/49] chore: Need to create the workflow in main so it can be updated and run in v2 branch (#127) * chore: Need to create the workflow in main so it can be updated and run in v2 branch * chore: Add newline --- .github/workflows/internal-image-publish.yaml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/internal-image-publish.yaml diff --git a/.github/workflows/internal-image-publish.yaml b/.github/workflows/internal-image-publish.yaml new file mode 100644 index 00000000..06b1886c --- /dev/null +++ b/.github/workflows/internal-image-publish.yaml @@ -0,0 +1,20 @@ +name: Internal Image Publish + +on: + workflow_dispatch: + inputs: + image_tag: + type: string + description: 'Tags for Images in GAR' + required: true + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + persist-credentials: false + fetch-depth: 0 From b272e14e93071e9cd3effe1b70e9900049dc7eb8 Mon Sep 17 00:00:00 2001 From: Daniel Panzella Date: Wed, 11 Feb 2026 15:05:35 -0800 Subject: [PATCH 06/49] chore: Add empty workflow so it can be run in a branch (#131) --- .github/workflows/internal-chart-publish.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/internal-chart-publish.yaml diff --git a/.github/workflows/internal-chart-publish.yaml b/.github/workflows/internal-chart-publish.yaml new file mode 100644 index 00000000..569c000e --- /dev/null +++ b/.github/workflows/internal-chart-publish.yaml @@ -0,0 +1,18 @@ +name: Internal Chart Publish + +on: + workflow_dispatch: + +jobs: + release: + name: Release + runs-on: ubuntu-latest + permissions: + contents: 'read' + id-token: 'write' + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + persist-credentials: false + fetch-depth: 0 From f3abfed197763a23e7e1ffdef42751c8e9608807 Mon Sep 17 00:00:00 2001 From: Kevin Chen <140976422+wandb-kc@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:33:55 -0500 Subject: [PATCH 07/49] remove aquasecurity (#142) --- .github/workflows/docker-build-scan.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/docker-build-scan.yml b/.github/workflows/docker-build-scan.yml index f11386cb..e5908770 100644 --- a/.github/workflows/docker-build-scan.yml +++ b/.github/workflows/docker-build-scan.yml @@ -25,13 +25,3 @@ jobs: tags: wandb/operator:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max - - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master - with: - image-ref: wandb/operator:${{ github.sha }} - format: "table" - exit-code: "1" - ignore-unfixed: true - vuln-type: "os,library" - severity: "HIGH,CRITICAL" From fcb72b8652c6370fb05aa6a805707540c1f57d81 Mon Sep 17 00:00:00 2001 From: Zachary Blasczyk <77289967+zacharyblasczyk@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:39:19 -0500 Subject: [PATCH 08/49] feat: Add OCI Helm chart registry support and upgrade to Helm v4 (#147) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add OCI Helm chart registry support and upgrade to Helm v4 Enable the operator to pull Helm charts from OCI-based registries via a new OCIRelease chart type, and upgrade the Helm SDK from v3.19.2 to v4.1.3 to align with the current Helm ecosystem. Helm v4 upgrade: - Update all import paths (chart/v2, release/v1, repo/v1, etc.) - Adapt to v4 API changes: Config.Init log param removed, Uninstall.Wait replaced by WaitStrategy, Entry field rename - Remove dead DownloadChart() code - Regenerate counterfeiter fakes OCI support: - New OCIRelease type in pkg/wandb/spec/charts/oci.go using Helm's native registry.Client.Pull() with in-memory chart loading - Custom ociurl validator using registry.IsOCI() - Supports authentication via direct credentials or K8s Secrets - PlainHTTP option for insecure/local registries - Registered in chart dispatcher between LocalRelease and RepoRelease - 21 new Ginkgo test specs covering validation, dispatcher routing, credential secrets, and error paths * fix: require OCI chart version and guard against nil pull result Reject tagless OCI refs at validation time instead of letting them fail at runtime on every reconcile. Also add a nil check on the pull result to prevent a panic on corrupted registry responses. * fix: move nil check before debug dereference, reject oci:// in RepoRelease - Move result.Chart nil guard above the debug log that dereferences it to prevent a nil-pointer panic when Debug is enabled. - Add nonociurl validator to RepoRelease.URL so oci:// URLs are explicitly rejected at validation time, not just by dispatcher ordering. - Fix misleading test: include name field so the test actually verifies scheme-level rejection rather than passing due to missing required field. * fix: use Accessor interface in isInstalled and parallel-safe env in tests Use release.NewAccessor() instead of type-asserting to *releasev1.Release in isInstalled(), making it resilient to future Helm release types. Replace os.Setenv with GinkgoT().Setenv() for parallel-safe test execution. * fix: add ContentCache to ChartDownloader for Helm v4 Helm v4's ChartDownloader requires the ContentCache field to be set, separate from RepositoryCache. Without it, DownloadTo() fails with "content cache must be set". Pass settings.ContentCache through from the cli.EnvSettings, which defaults to $HELM_CONTENT_CACHE or ~/.cache/helm/content. * fix: disable SSA for Helm v4 to preserve client-side apply semantics Helm v4 defaults to Server-Side Apply (SSA) for new installs (HIP-0023). SSA uses field-level merge instead of full object replacement, which breaks when upgrading charts that change a Deployment's strategy. Specifically: when a chart creates a Deployment without specifying strategy (defaulting to RollingUpdate), the API server auto-populates strategy.rollingUpdate with defaults that no field manager owns. A subsequent SSA upgrade setting strategy.type=Recreate cannot remove the unowned rollingUpdate field, and the API server rejects the result: "spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy type is 'Recreate'" Disable SSA (ServerSideApply=false) on Install, Upgrade, and Rollback to use client-side apply (full object replacement), which cleans up stale fields and matches Helm v3 behavior. Use HookOnlyStrategy instead of StatusWatcherStrategy/LegacyStrategy. The operator's own reconcile loop already monitors resource readiness. Having Helm also wait via StatusWatcherStrategy causes the reconciler to re-enter while a Helm wait is in progress, producing "another operation (install/upgrade/rollback) is in progress" errors. HookOnlyStrategy waits only for pre/post hooks, then returns control to the operator. * chore: update test manifest for Helm v4 testing Fix app.resources.memory indentation (was sibling of requests, not child). Bump chart to v0.41.3 (v0.32.4 has duplicate env keys rejected by Helm v4). Add glue strategy and resource limits. Disable settingsMigrationJob. * chore: add orbstack to Tiltfile allowed contexts * chore: go mod tidy and update dependencies Update controller-runtime v0.23.1→v0.23.3, validator v10.26.0→v10.30.2, gomega v1.39.0→v1.39.1, and transitive dependencies. * fix: use LegacyStrategy for Uninstall to wait for resource deletion HookOnlyStrategy's WaitForDelete() is a no-op, which means Uninstall returns success while resources are still terminating. This could cause Prune() to report success prematurely. Use LegacyStrategy for Uninstall to actually wait for resources to be deleted, while keeping HookOnlyStrategy for Install/Upgrade/Rollback where the operator's reconcile loop handles readiness. * cleanup * feat: add local OCI registry testing infrastructure Add Makefile targets for local OCI registry with basic auth: - make local-registry: starts registry:2 on port 5050 with htpasswd auth - make local-registry-push: pulls chart from charts.wandb.ai and pushes to local registry (version read from oci.yaml manifest) - make local-registry-secret: creates K8s secret with registry creds - make local-registry-stop: tears down the registry Add hack/testing-manifests/wandb/oci.yaml for testing OCI chart pulls with credential secret authentication. * fix: upgrade google.golang.org/grpc to v1.79.3 for CVE-2026-33186 Fixes GO-2026-4762 / GHSA-p77j-4mvh-x3m3: authorization bypass in gRPC-Go caused by improper validation of the HTTP/2 :path pseudo-header. All versions before v1.79.3 are affected. * fix: use scheme-based TLS verification instead of unconditional bypass The getter option WithInsecureSkipVerifyTLS was hard-coded to true, disabling TLS certificate validation for all chart downloads including HTTPS. Use the already-computed entry.InsecureSkipTLSVerify which is true only for HTTP URLs, preserving TLS verification for HTTPS. * fix: validate credential secret keys exist before using them When loading credentials from a Kubernetes secret, check that the expected keys exist and are non-empty. Previously, missing keys would silently produce empty username/password strings, leading to opaque auth failures when pulling from the registry. * fix: handle stopped registry containers and explicit namespace for secret Remove stopped containers before starting a new registry to prevent name conflicts. Add explicit namespace (WANDB_NAMESPACE, default: default) to the credential secret creation. * fix: reject partial OCI auth credentials (username xor password) When only one of username or password is provided, the code silently falls back to anonymous pull. Fail fast with a clear error so misconfiguration is immediately visible. * chore: add orbstack to docs and sample tilt settings Add orbstack to the allowed Kubernetes contexts in README and tilt-settings.sample.json. Minor formatting cleanup in README. * fix(chart): Use helm to validate references for versions and digest refs * fix(dependency): Bump helm go module for security fix * fix(dependency): Bump golang.opentelemetry.io packages for security fix * fix(deps): tidy go.sum to drop superseded module versions Removes stale entries for older grpc-gateway, otelhttp, otel, and x/net/oauth2 versions that were no longer referenced after recent dependency bumps, resolving the Dependency Check CI job. --------- Co-authored-by: casey-coreweave --- Makefile | 50 +++ README.md | 30 +- Tiltfile | 1 + go.mod | 155 ++++---- go.sum | 444 ++++++++++------------ hack/testing-manifests/wandb/default.yaml | 9 +- hack/testing-manifests/wandb/oci.yaml | 74 ++++ pkg/helm/chart.go | 106 ++---- pkg/wandb/spec/charts/charts.go | 1 + pkg/wandb/spec/charts/local.go | 4 +- pkg/wandb/spec/charts/oci.go | 223 +++++++++++ pkg/wandb/spec/charts/oci_test.go | 349 +++++++++++++++++ pkg/wandb/spec/charts/repo.go | 28 +- pkg/wandb/spec/charts/repo_test.go | 4 +- pkg/wandb/spec/config.go | 4 +- pkg/wandb/spec/spec.go | 2 +- pkg/wandb/spec/specfakes/fake_chart.go | 22 +- tilt-settings.sample.json | 7 +- 18 files changed, 1094 insertions(+), 419 deletions(-) create mode 100644 hack/testing-manifests/wandb/oci.yaml create mode 100644 pkg/wandb/spec/charts/oci.go create mode 100644 pkg/wandb/spec/charts/oci_test.go diff --git a/Makefile b/Makefile index fdefbd90..c3633e82 100644 --- a/Makefile +++ b/Makefile @@ -90,6 +90,56 @@ lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes lint-config: golangci-lint ## Verify golangci-lint linter configuration $(GOLANGCI_LINT) config verify +REGISTRY_PORT ?= 5050 +REGISTRY_NAME ?= registry +REGISTRY_USER ?= admin +REGISTRY_PASS ?= admin123 +REGISTRY_AUTH_DIR ?= /tmp/registry-auth +CHART_REPO ?= https://charts.wandb.ai +CHART_NAME ?= operator-wandb +OCI_MANIFEST ?= hack/testing-manifests/wandb/oci.yaml +CHART_VERSION ?= $(shell grep 'version:' $(OCI_MANIFEST) | head -1 | sed 's/.*"\(.*\)"/\1/') + +.PHONY: local-registry +local-registry: ## Start a local OCI registry with basic auth for testing. + @if $(CONTAINER_TOOL) ps --filter name=$(REGISTRY_NAME) --format '{{.Names}}' | grep -q $(REGISTRY_NAME); then \ + echo "Registry already running on port $(REGISTRY_PORT)"; \ + else \ + $(CONTAINER_TOOL) rm -f $(REGISTRY_NAME) >/dev/null 2>&1 || true; \ + mkdir -p $(REGISTRY_AUTH_DIR) && \ + $(CONTAINER_TOOL) run --rm --entrypoint sh registry:2 -c \ + "apk add --no-cache apache2-utils >/dev/null 2>&1 && htpasswd -Bbn $(REGISTRY_USER) $(REGISTRY_PASS)" \ + > $(REGISTRY_AUTH_DIR)/htpasswd && \ + $(CONTAINER_TOOL) run -d --name $(REGISTRY_NAME) -p $(REGISTRY_PORT):5000 \ + -v $(REGISTRY_AUTH_DIR):/auth \ + -e REGISTRY_AUTH=htpasswd \ + -e REGISTRY_AUTH_HTPASSWD_REALM=Registry \ + -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \ + registry:2; \ + echo "Registry started on port $(REGISTRY_PORT) (user: $(REGISTRY_USER))"; \ + fi + +.PHONY: local-registry-push +local-registry-push: local-registry ## Push a chart from the wandb helm repo to the local OCI registry. + @tmpdir=$$(mktemp -d) && \ + helm pull $(CHART_NAME) --repo $(CHART_REPO) --version $(CHART_VERSION) -d $$tmpdir && \ + helm push $$tmpdir/$(CHART_NAME)-$(CHART_VERSION).tgz oci://localhost:$(REGISTRY_PORT)/wandb \ + --plain-http --username $(REGISTRY_USER) --password $(REGISTRY_PASS) && \ + rm -rf $$tmpdir + +WANDB_NAMESPACE ?= default + +.PHONY: local-registry-secret +local-registry-secret: ## Create a Kubernetes secret with registry credentials for the operator. + @kubectl -n $(WANDB_NAMESPACE) create secret generic oci-registry-creds \ + --from-literal=HELM_USERNAME=$(REGISTRY_USER) \ + --from-literal=HELM_PASSWORD=$(REGISTRY_PASS) \ + --dry-run=client -o yaml | kubectl apply -f - + +.PHONY: local-registry-stop +local-registry-stop: ## Stop and remove the local OCI registry. + @$(CONTAINER_TOOL) rm -f $(REGISTRY_NAME) 2>/dev/null || true + ##@ Build .PHONY: build diff --git a/README.md b/README.md index ef420eb2..9102ea31 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # operator + // TODO(user): Add simple overview of use/purpose ## Description + // TODO(user): An in-depth paragraph about your project and overview of use ## Development @@ -13,19 +15,22 @@ #### Install Kind -A kubernetes cluster is required to run the operator. [kind](https://kind.sigs.k8s.io/) is recommended for local development. Install `kind` and -create a cluster: +A kubernetes cluster is required to run the operator. +[kind](https://kind.sigs.k8s.io/) is recommended for local development. Install +`kind` and create a cluster: ```bash brew install kind kind create cluster ``` -This will create a new kind cluster with the name `kind`. The kubernetes context will be called `kind-kind`. +This will create a new kind cluster with the name `kind`. The kubernetes context +will be called `kind-kind`. #### Install Tilt -[Tilt](https://tilt.dev/) is a tool for local development of Kubernetes applications. Install `tilt`: +[Tilt](https://tilt.dev/) is a tool for local development of Kubernetes +applications. Install `tilt`: ```bash brew install tilt @@ -33,7 +38,8 @@ brew install tilt #### Install Kubebuilder -[kubebuilder](https://book.kubebuilder.io/quick-start.html) is a tool for building Kubernetes operators. Install `kubebuilder`: +[kubebuilder](https://book.kubebuilder.io/quick-start.html) is a tool for +building Kubernetes operators. Install `kubebuilder`: ```bash brew install kubebuilder @@ -43,17 +49,21 @@ brew install kubebuilder #### Tilt Settings -There are settings for Tilt that can be configured using a `tilt-settings.json` file. The settings file is not checked -into source control. A sample settings file is provided in `tilt-settings.sample.json`. To use the sample settings file, -copy it to `tilt-settings.json` +There are settings for Tilt that can be configured using a `tilt-settings.json` +file. The settings file is not checked into source control. A sample settings +file is provided in `tilt-settings.sample.json`. To use the sample settings +file, copy it to `tilt-settings.json` -By default, Tilt is configured to only allow connections to the following Kubernetes contexts: +By default, Tilt is configured to only allow connections to the following +Kubernetes contexts: - `docker-desktop` - `kind-kind` - `minikube` +- `orbstack` -Please add any additional contexts to the `allowedContexts` list in your `tilt-settings.json` file. +Please add any additional contexts to the `allowedContexts` list in your +`tilt-settings.json` file. #### Running Tilt diff --git a/Tiltfile b/Tiltfile index 3c3d8889..8db8086e 100644 --- a/Tiltfile +++ b/Tiltfile @@ -4,6 +4,7 @@ settings = { "docker-desktop", "minikube", "kind-kind", + "orbstack", ], "installMinio": True, "installWandb": True, diff --git a/go.mod b/go.mod index 09004345..631bdc9f 100644 --- a/go.mod +++ b/go.mod @@ -5,165 +5,162 @@ go 1.25.3 godebug default=go1.25 require ( - github.com/go-logr/logr v1.4.2 - github.com/go-playground/validator/v10 v10.26.0 + github.com/go-logr/logr v1.4.3 + github.com/go-playground/validator/v10 v10.30.2 github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 - github.com/onsi/ginkgo/v2 v2.21.0 - github.com/onsi/gomega v1.36.1 + github.com/onsi/ginkgo/v2 v2.28.1 + github.com/onsi/gomega v1.39.1 github.com/pkg/errors v0.9.1 - helm.sh/helm/v3 v3.19.2 - k8s.io/api v0.34.0 - k8s.io/apimachinery v0.34.0 - k8s.io/client-go v0.34.0 - sigs.k8s.io/controller-runtime v0.20.0 + helm.sh/helm/v4 v4.1.4 + k8s.io/api v0.35.1 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 + oras.land/oras-go/v2 v2.6.0 + sigs.k8s.io/controller-runtime v0.23.3 ) require ( - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.1 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/BurntSushi/toml v1.5.0 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/containerd/containerd v1.7.29 // indirect - github.com/containerd/errdefs v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect - github.com/cyphar/filepath-securejoin v0.6.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/fatih/color v1.13.0 // indirect + github.com/extism/go-sdk v1.7.1 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fluxcd/cli-utils v0.37.2-flux.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/go-errors/errors v1.4.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/gogo/protobuf v1.3.2 // indirect + github.com/gofrs/flock v0.13.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.26.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect + github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect + github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/spdystream v0.5.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/rubenv/sql-migrate v1.8.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/rubenv/sql-migrate v1.8.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect + github.com/tetratelabs/wazero v1.11.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/sdk v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect - go.opentelemetry.io/proto/otlp v1.5.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.43.0 // indirect + golang.org/x/crypto v0.49.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/mod v0.28.0 // indirect - golang.org/x/net v0.46.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/term v0.36.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.37.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect - google.golang.org/grpc v1.72.1 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + golang.org/x/tools v0.42.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.80.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.34.0 // indirect - k8s.io/apiserver v0.34.0 // indirect - k8s.io/cli-runtime v0.34.0 // indirect - k8s.io/component-base v0.34.0 // indirect + k8s.io/apiextensions-apiserver v0.35.1 // indirect + k8s.io/apiserver v0.35.1 // indirect + k8s.io/cli-runtime v0.35.1 // indirect + k8s.io/component-base v0.35.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect - k8s.io/kubectl v0.34.0 // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect - oras.land/oras-go/v2 v2.6.0 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/kubectl v0.35.1 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect - sigs.k8s.io/kustomize/api v0.20.1 // indirect - sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/kustomize/api v0.21.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index f58caa84..e1fbcba0 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= @@ -8,8 +8,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9 github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= @@ -22,10 +22,10 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -34,28 +34,21 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE= -github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= -github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= -github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= -github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -74,63 +67,75 @@ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE= +github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= -github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= +github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= -github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/fluxcd/cli-utils v0.37.2-flux.1 h1:tQ588ghtRN+E+kHq415FddfqA9v4brn/1WWgrP6rQR0= +github.com/fluxcd/cli-utils v0.37.2-flux.1/go.mod h1:LcWSu1NYET8d8U7O326RhEm5JkQXCMK6ITu4G1CT02c= +github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= +github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -144,50 +149,42 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b h1:ogbOPx86mIhFy764gGkqnkFC8m5PJA7sPzlk9ppLVQA= +github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -202,22 +199,23 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 h1:yVCLo4+ACVroOEr4iFU1iH46Ldlzz2rTuu18Ra7M8sU= github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2/go.mod h1:VzB2VoMh1Y32/QqDfg9ZJYHj99oM4LiGtqPZydTiQSQ= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -226,8 +224,6 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -240,20 +236,16 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= -github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= -github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= -github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -261,40 +253,42 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= -github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0= +github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -314,136 +308,120 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= +github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= +github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= -go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= -go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= -go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= -go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= -go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= -go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= -go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= -go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI= +go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac= +go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g= +go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= +go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo= +go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= +go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4= +go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI= +go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= -gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -451,45 +429,45 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -helm.sh/helm/v3 v3.19.2 h1:psQjaM8aIWrSVEly6PgYtLu/y6MRSmok4ERiGhZmtUY= -helm.sh/helm/v3 v3.19.2/go.mod h1:gX10tB5ErM+8fr7bglUUS/UfTOO8UUTYWIBH1IYNnpE= -k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= -k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= -k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= -k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= -k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= -k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg= -k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ= -k8s.io/cli-runtime v0.34.0 h1:N2/rUlJg6TMEBgtQ3SDRJwa8XyKUizwjlOknT1mB2Cw= -k8s.io/cli-runtime v0.34.0/go.mod h1:t/skRecS73Piv+J+FmWIQA2N2/rDjdYSQzEE67LUUs8= -k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= -k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= -k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8= -k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg= +helm.sh/helm/v4 v4.1.4 h1:zwTrNkalG4f7SYigRSdQnYrTj0QEz1qzetzAlYoDVSo= +helm.sh/helm/v4 v4.1.4/go.mod h1:5dSo8rRgn3OTkDAc/k0Ipw5/Q+BlqKIKZwa0XwSiINI= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs= +k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4= +k8s.io/cli-runtime v0.35.1 h1:uKcXFe8J7AMAM4Gm2JDK4mp198dBEq2nyeYtO+JfGJE= +k8s.io/cli-runtime v0.35.1/go.mod h1:55/hiXIq1C8qIJ3WBrWxEwDLdHQYhBNRdZOz9f7yvTw= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE= +k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= -k8s.io/kubectl v0.34.0 h1:NcXz4TPTaUwhiX4LU+6r6udrlm0NsVnSkP3R9t0dmxs= -k8s.io/kubectl v0.34.0/go.mod h1:bmd0W5i+HuG7/p5sqicr0Li0rR2iIhXL0oUyLF3OjR4= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/kubectl v0.35.1 h1:zP3Er8C5i1dcAFUMh9Eva0kVvZHptXIn/+8NtRWMxwg= +k8s.io/kubectl v0.35.1/go.mod h1:cQ2uAPs5IO/kx8R5s5J3Ihv3VCYwrx0obCXum0CvnXo= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.20.0 h1:jjkMo29xEXH+02Md9qaVXfEIaMESSpy3TBWPrsfQkQs= -sigs.k8s.io/controller-runtime v0.20.0/go.mod h1:BrP3w158MwvB3ZbNpaAcIKkHQ7YGpYnzpoSTZ8E14WU= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= -sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= -sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= -sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= +sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= +sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= +sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/hack/testing-manifests/wandb/default.yaml b/hack/testing-manifests/wandb/default.yaml index cc10827c..0c16dbf9 100644 --- a/hack/testing-manifests/wandb/default.yaml +++ b/hack/testing-manifests/wandb/default.yaml @@ -13,7 +13,7 @@ spec: chart: url: https://charts.wandb.ai name: "operator-wandb" - version: "0.32.4" + version: "0.41.3" values: global: bucket: @@ -27,7 +27,7 @@ spec: resources: requests: cpu: "100m" - memory: "128Mi" + memory: "128Mi" parquet: resources: @@ -66,4 +66,7 @@ spec: memory: "128Mi" reloader: - install: true \ No newline at end of file + install: true + + settingsMigrationJob: + install: false diff --git a/hack/testing-manifests/wandb/oci.yaml b/hack/testing-manifests/wandb/oci.yaml new file mode 100644 index 00000000..49f968e1 --- /dev/null +++ b/hack/testing-manifests/wandb/oci.yaml @@ -0,0 +1,74 @@ +--- +apiVersion: apps.wandb.com/v1 +kind: WeightsAndBiases +metadata: + labels: + app.kubernetes.io/name: weightsandbiases + app.kubernetes.io/instance: weightsandbiases-sample + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: operator + name: wandb-default +spec: + chart: + url: oci://host.internal:5050/wandb/operator-wandb + version: "0.41.3" + plainHTTP: true + credentialSecret: + name: oci-registry-creds + values: + global: + bucket: + provider: "s3" + name: "minio.minio.svc.cluster.local:9000/bucket" + region: "us-east-1" + accessKey: "minio" + secretKey: "minio123" + + app: + resources: + requests: + cpu: "100m" + memory: "128Mi" + + parquet: + resources: + requests: + cpu: "100m" + memory: "128Mi" + + weave: + resources: + requests: + cpu: "100m" + memory: "128Mi" + + console: + resources: + requests: + cpu: "100m" + memory: "128Mi" + + ingress: + install: false + create: false + + mysql: + install: true + resources: + requests: + cpu: "100m" + memory: "128Mi" + + redis: + install: true + resources: + requests: + cpu: "100m" + memory: "128Mi" + + reloader: + install: true + + settingsMigrationJob: + install: false diff --git a/pkg/helm/chart.go b/pkg/helm/chart.go index 68f373bb..003c3b6b 100644 --- a/pkg/helm/chart.go +++ b/pkg/helm/chart.go @@ -4,14 +4,13 @@ import ( "fmt" "time" - "helm.sh/helm/v3/pkg/action" - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/chartutil" - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/getter" - "helm.sh/helm/v3/pkg/release" - "helm.sh/helm/v3/pkg/releaseutil" - "helm.sh/helm/v3/pkg/repo" + "helm.sh/helm/v4/pkg/action" + chart "helm.sh/helm/v4/pkg/chart/v2" + chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + "helm.sh/helm/v4/pkg/cli" + "helm.sh/helm/v4/pkg/kube" + "helm.sh/helm/v4/pkg/release" + releasecommon "helm.sh/helm/v4/pkg/release/common" ) const ( @@ -19,27 +18,23 @@ const ( maxReleasesToKeep = 10 ) -var ( - noopLogger = func(_ string, _ ...interface{}) {} -) - type ActionableChartInterface interface { Apply( chart *chart.Chart, values map[string]interface{}, - ) (*release.Release, error) + ) (release.Releaser, error) Install( chart *chart.Chart, values map[string]interface{}, - ) (*release.Release, error) - History() ([]*release.Release, error) + ) (release.Releaser, error) + History() ([]release.Releaser, error) Rollback(version int) error - Upgrade(chart *chart.Chart, values map[string]interface{}) (*release.Release, error) + Upgrade(chart *chart.Chart, values map[string]interface{}) (release.Releaser, error) Uninstall() (*release.UninstallReleaseResponse, error) - GetRelease(version int) (*release.Release, error) + GetRelease(version int) (release.Releaser, error) } -// GetConfig returns a helm action configuration. Namespace is used to determine +// InitConfig returns a helm action configuration. Namespace is used to determine // where to store the versions func InitConfig(namespace string) (*cli.EnvSettings, *action.Configuration, error) { settings := cli.New() @@ -49,47 +44,11 @@ func InitConfig(namespace string) (*cli.EnvSettings, *action.Configuration, erro settings.RESTClientGetter(), settings.Namespace(), secretsStorageDriver, - noopLogger, ) config.Releases.MaxHistory = maxReleasesToKeep return settings, config, err } -func DownloadChart(repoURL string, name string) (string, error) { - settings := cli.New() - providers := getter.All(settings) - - entry := new(repo.Entry) - entry.URL = repoURL - entry.Name = name - - file := repo.NewFile() - file.Update(entry) - - chartRepo, err := repo.NewChartRepository(entry, providers) - if err != nil { - return "", err - } - - _, err = chartRepo.DownloadIndexFile() - if err != nil { - return "", err - } - - chartURL, err := repo.FindChartInRepoURL( - repoURL, name, - "", "", "", "", - providers, - ) - if err != nil { - return "", err - } - - client := action.NewPull() - client.Settings = settings - return client.Run(chartURL) -} - func NewActionableChart(releaseName string, namespace string) (*ActionableChart, error) { if err := chartutil.ValidateReleaseName(releaseName); err != nil { return nil, fmt.Errorf("release name %q", releaseName) @@ -119,18 +78,29 @@ func (c *ActionableChart) isInstalled() bool { return false } - releaseutil.Reverse(h, releaseutil.SortByRevision) - rel := h[0] - st := rel.Info.Status - - return st != release.StatusUninstalled + // Find the release with the highest revision using the Accessor interface, + // which handles any concrete release type (not just v1). + var latest release.Accessor + for _, r := range h { + acc, err := release.NewAccessor(r) + if err != nil { + continue + } + if latest == nil || acc.Version() > latest.Version() { + latest = acc + } + } + if latest == nil { + return false + } + return latest.Status() != releasecommon.StatusUninstalled.String() } func (c *ActionableChart) Apply( chart *chart.Chart, values map[string]interface{}, -) (*release.Release, error) { +) (release.Releaser, error) { if c.isInstalled() { return c.Upgrade(chart, values) } @@ -140,36 +110,42 @@ func (c *ActionableChart) Apply( func (c *ActionableChart) Install( chart *chart.Chart, values map[string]interface{}, -) (*release.Release, error) { +) (release.Releaser, error) { client := action.NewInstall(c.config) client.ReleaseName = c.releaseName client.Namespace = c.namespace + client.WaitStrategy = kube.HookOnlyStrategy + client.ServerSideApply = false return client.Run(chart, values) } -func (c *ActionableChart) History() ([]*release.Release, error) { +func (c *ActionableChart) History() ([]release.Releaser, error) { return c.config.Releases.History(c.releaseName) } func (c *ActionableChart) Rollback(version int) error { client := action.NewRollback(c.config) + client.WaitStrategy = kube.HookOnlyStrategy + client.ServerSideApply = "false" return client.Run(c.releaseName) } -func (c *ActionableChart) Upgrade(chart *chart.Chart, values map[string]interface{}) (*release.Release, error) { +func (c *ActionableChart) Upgrade(chart *chart.Chart, values map[string]interface{}) (release.Releaser, error) { client := action.NewUpgrade(c.config) client.Namespace = c.namespace client.MaxHistory = maxReleasesToKeep + client.WaitStrategy = kube.HookOnlyStrategy + client.ServerSideApply = "false" return client.Run(c.releaseName, chart, values) } func (c *ActionableChart) Uninstall() (*release.UninstallReleaseResponse, error) { client := action.NewUninstall(c.config) - client.Wait = true + client.WaitStrategy = kube.LegacyStrategy client.Timeout = 600 * time.Second return client.Run(c.releaseName) } -func (c *ActionableChart) GetRelease(version int) (*release.Release, error) { +func (c *ActionableChart) GetRelease(version int) (release.Releaser, error) { return c.config.Releases.Get(c.releaseName, version) } diff --git a/pkg/wandb/spec/charts/charts.go b/pkg/wandb/spec/charts/charts.go index 286c454e..82d5795c 100644 --- a/pkg/wandb/spec/charts/charts.go +++ b/pkg/wandb/spec/charts/charts.go @@ -33,6 +33,7 @@ type ValidatableRelease interface { func Get(maybeRelease interface{}) spec.Chart { releases := []ValidatableRelease{ new(LocalRelease), + new(OCIRelease), new(RepoRelease), } diff --git a/pkg/wandb/spec/charts/local.go b/pkg/wandb/spec/charts/local.go index 9153944d..8471828f 100644 --- a/pkg/wandb/spec/charts/local.go +++ b/pkg/wandb/spec/charts/local.go @@ -9,8 +9,8 @@ import ( v1 "github.com/wandb/operator/api/v1" "github.com/wandb/operator/pkg/helm" "github.com/wandb/operator/pkg/wandb/spec" - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/chart/loader" + chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/loader" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/pkg/wandb/spec/charts/oci.go b/pkg/wandb/spec/charts/oci.go new file mode 100644 index 00000000..5dca4582 --- /dev/null +++ b/pkg/wandb/spec/charts/oci.go @@ -0,0 +1,223 @@ +package charts + +import ( + "bytes" + "context" + "fmt" + "net/url" + "strings" + + "github.com/go-playground/validator/v10" + v1 "github.com/wandb/operator/api/v1" + "github.com/wandb/operator/pkg/helm" + "github.com/wandb/operator/pkg/wandb/spec" + chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/chart/v2/loader" + "helm.sh/helm/v4/pkg/registry" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + orasregistry "oras.land/oras-go/v2/registry" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" +) + +// OCIRelease pulls a Helm chart from an OCI-based registry. +// The URL must use the oci:// scheme (e.g., oci://ghcr.io/wandb/helm-charts/operator-wandb). +type OCIRelease struct { + URL string `validate:"required,ociurl" json:"url"` + Version string `validate:"ociversion" json:"version,omitempty"` + + CredentialSecret *CredentialSecret `json:"credentialSecret,omitempty"` + Password string `json:"password"` + Username string `json:"username"` + + PlainHTTP bool `json:"plainHTTP,omitempty"` + Debug bool `json:"debug"` +} + +func validateOCIURL(fl validator.FieldLevel) bool { + return registry.IsOCI(fl.Field().String()) +} + +func validateOCIVersion(fl validator.FieldLevel) bool { + release, ok := fl.Parent().Interface().(OCIRelease) + if !ok { + return false + } + if release.Version != "" { + return true + } + + parsedRef, err := orasregistry.ParseReference(strings.TrimPrefix( + release.URL, + fmt.Sprintf("%s://", registry.OCIScheme), + )) + if err != nil { + return false + } + + return parsedRef.Reference != "" +} + +func (c OCIRelease) Validate() error { + v := validator.New() + v.RegisterValidation("ociurl", validateOCIURL) + v.RegisterValidation("ociversion", validateOCIVersion) + return v.Struct(c) +} + +func (r OCIRelease) Chart() (*chart.Chart, error) { + return r.pullChart() +} + +func (r OCIRelease) pullReference(registryClient *registry.Client) (string, error) { + parsedURL, err := url.Parse(r.URL) + if err != nil { + return "", fmt.Errorf("invalid OCI URL %q: %w", r.URL, err) + } + + _, normalizedRef, err := registryClient.ValidateReference(r.URL, r.Version, parsedURL) + if err != nil { + return "", fmt.Errorf("failed to validate OCI reference %s: %w", r.URL, err) + } + + return normalizedRef.String(), nil +} + +func (r OCIRelease) pullChart() (*chart.Chart, error) { + log := ctrllog.Log.WithName("chart-oci") + + opts := []registry.ClientOption{ + registry.ClientOptEnableCache(true), + } + if r.Debug { + opts = append(opts, registry.ClientOptDebug(true)) + } + if (r.Username == "") != (r.Password == "") { + return nil, fmt.Errorf("both username and password must be set together for OCI basic auth") + } + if r.Username != "" && r.Password != "" { + opts = append(opts, registry.ClientOptBasicAuth(r.Username, r.Password)) + } + if r.PlainHTTP { + opts = append(opts, registry.ClientOptPlainHTTP()) + } + + registryClient, err := registry.NewClient(opts...) + if err != nil { + log.Error(err, "Failed to create registry client") + return nil, fmt.Errorf("failed to create registry client: %w", err) + } + + ref, err := r.pullReference(registryClient) + if err != nil { + log.Error(err, "Failed to normalize OCI reference", "url", r.URL, "version", r.Version) + return nil, err + } + + if r.Debug { + log.Info("Pulling OCI chart", "ref", ref) + } + + result, err := registryClient.Pull(ref) + if err != nil { + log.Error(err, "Failed to pull chart", "ref", ref) + return nil, fmt.Errorf("failed to pull chart from %s: %w", r.URL, err) + } + + if result.Chart == nil { + return nil, fmt.Errorf("registry returned empty chart for %s", r.URL) + } + + if r.Debug { + log.Info("Chart pulled successfully", + "name", result.Chart.Meta.Name, + "version", result.Chart.Meta.Version, + "size", result.Chart.Size) + } + + // Load the chart directly from the pulled bytes + chrt, err := loader.LoadArchive(bytes.NewReader(result.Chart.Data)) + if err != nil { + log.Error(err, "Failed to load chart archive") + return nil, fmt.Errorf("failed to load chart archive: %w", err) + } + + return chrt, nil +} + +func (r *OCIRelease) getActionableChart(wandb *v1.WeightsAndBiases) (*helm.ActionableChart, error) { + namespace := wandb.GetNamespace() + releaseName := wandb.GetName() + return helm.NewActionableChart(releaseName, namespace) +} + +func (r OCIRelease) Apply( + ctx context.Context, + c client.Client, + wandb *v1.WeightsAndBiases, + scheme *runtime.Scheme, + config spec.Values, +) error { + log := ctrllog.Log.WithName("chart-oci") + if r.CredentialSecret != nil { + if r.CredentialSecret.UsernameKey == "" { + r.CredentialSecret.UsernameKey = CredentialUsernameKey + } + if r.CredentialSecret.PasswordKey == "" { + r.CredentialSecret.PasswordKey = CredentialPasswordKey + } + log.Info("Retrieving credentials from secret", + "name", r.CredentialSecret.Name, + "usernameKey", r.CredentialSecret.UsernameKey, + "passwordKey", r.CredentialSecret.PasswordKey) + + secret := &corev1.Secret{} + err := c.Get(ctx, client.ObjectKey{Name: r.CredentialSecret.Name, Namespace: wandb.Namespace}, secret) + if err != nil { + log.Error(err, "Failed to get credentials from secret") + return err + } + usernameBytes, ok := secret.Data[r.CredentialSecret.UsernameKey] + if !ok || len(usernameBytes) == 0 { + return fmt.Errorf("credential secret %s/%s missing key %q", + wandb.Namespace, r.CredentialSecret.Name, r.CredentialSecret.UsernameKey) + } + passwordBytes, ok := secret.Data[r.CredentialSecret.PasswordKey] + if !ok || len(passwordBytes) == 0 { + return fmt.Errorf("credential secret %s/%s missing key %q", + wandb.Namespace, r.CredentialSecret.Name, r.CredentialSecret.PasswordKey) + } + r.Username = string(usernameBytes) + r.Password = string(passwordBytes) + } + + chrt, err := r.pullChart() + if err != nil { + return err + } + + actionableChart, err := r.getActionableChart(wandb) + if err != nil { + return err + } + + _, err = actionableChart.Apply(chrt, config) + return err +} + +func (r OCIRelease) Prune( + ctx context.Context, + c client.Client, + wandb *v1.WeightsAndBiases, + scheme *runtime.Scheme, + _ spec.Values, +) error { + actionableChart, err := r.getActionableChart(wandb) + if err != nil { + return err + } + + _, err = actionableChart.Uninstall() + return err +} diff --git a/pkg/wandb/spec/charts/oci_test.go b/pkg/wandb/spec/charts/oci_test.go new file mode 100644 index 00000000..f56dea61 --- /dev/null +++ b/pkg/wandb/spec/charts/oci_test.go @@ -0,0 +1,349 @@ +package charts + +import ( + "context" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "github.com/wandb/operator/api/v1" + "github.com/wandb/operator/pkg/wandb/spec" + "helm.sh/helm/v4/pkg/registry" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var _ = Describe("OCIRelease", func() { + var ociRelease *OCIRelease + + BeforeEach(func() { + ociRelease = &OCIRelease{ + URL: "oci://ghcr.io/wandb/helm-charts/operator-wandb", + Version: "1.0.0", + Debug: false, + } + }) + + Describe("Validate", func() { + Context("with valid OCI URL", func() { + It("should validate successfully", func() { + err := ociRelease.Validate() + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("with various valid OCI URLs", func() { + DescribeTable("should validate successfully", + func(url string) { + ociRelease.URL = url + err := ociRelease.Validate() + Expect(err).NotTo(HaveOccurred()) + }, + Entry("ghcr.io", "oci://ghcr.io/wandb/charts/wandb"), + Entry("docker.io", "oci://docker.io/library/nginx"), + Entry("custom registry with port", "oci://registry.example.com:5000/charts/wandb"), + Entry("tagged reference without version", "oci://ghcr.io/wandb/charts/wandb:1.0.0"), + Entry("digest reference without version", "oci://ghcr.io/wandb/charts/wandb@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888"), + ) + }) + + Context("with missing version", func() { + It("should fail validation for unqualified repository URLs", func() { + ociRelease.Version = "" + err := ociRelease.Validate() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Version")) + }) + + It("should allow tagged OCI URLs", func() { + ociRelease.URL = "oci://ghcr.io/wandb/charts/wandb:1.0.0" + ociRelease.Version = "" + err := ociRelease.Validate() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should allow digest OCI URLs", func() { + ociRelease.URL = "oci://ghcr.io/wandb/charts/wandb@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888" + ociRelease.Version = "" + err := ociRelease.Validate() + Expect(err).NotTo(HaveOccurred()) + }) + }) + + Context("with missing URL", func() { + It("should fail validation", func() { + ociRelease.URL = "" + err := ociRelease.Validate() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("URL")) + }) + }) + + Context("with non-OCI URL", func() { + DescribeTable("should fail validation", + func(url string) { + ociRelease.URL = url + err := ociRelease.Validate() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("URL")) + }, + Entry("https URL", "https://charts.example.com"), + Entry("http URL", "http://charts.example.com"), + Entry("plain hostname", "ghcr.io/wandb/charts/wandb"), + Entry("empty scheme", "://ghcr.io/wandb/charts/wandb"), + ) + }) + }) + + Describe("Chart dispatcher", func() { + It("should return OCIRelease for OCI URL", func() { + input := map[string]interface{}{ + "url": "oci://ghcr.io/wandb/charts/wandb", + "version": "1.0.0", + } + result := Get(input) + Expect(result).NotTo(BeNil()) + Expect(result).To(BeAssignableToTypeOf(&OCIRelease{})) + }) + + It("should return RepoRelease for HTTPS URL", func() { + input := map[string]interface{}{ + "url": "https://charts.example.com", + "name": "wandb", + } + result := Get(input) + Expect(result).NotTo(BeNil()) + Expect(result).To(BeAssignableToTypeOf(&RepoRelease{})) + }) + + It("should return LocalRelease for path", func() { + input := map[string]interface{}{ + "path": "/opt/charts/wandb.tgz", + } + result := Get(input) + Expect(result).NotTo(BeNil()) + Expect(result).To(BeAssignableToTypeOf(&LocalRelease{})) + }) + + It("should not match OCI URL as RepoRelease even with name field", func() { + input := map[string]interface{}{ + "url": "oci://ghcr.io/wandb/charts/wandb", + "name": "wandb", + "version": "1.0.0", + } + release := new(RepoRelease) + err := Is(release, input) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("pullChart", func() { + It("should return error for unreachable registry", func() { + ociRelease.URL = "oci://localhost:1/nonexistent/chart" + ociRelease.PlainHTTP = true + _, err := ociRelease.pullChart() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to pull chart")) + }) + }) + + Describe("pullReference", func() { + var registryClient *registry.Client + + BeforeEach(func() { + var err error + registryClient, err = registry.NewClient() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should preserve an explicit tag from the URL", func() { + ociRelease.URL = "oci://ghcr.io/wandb/charts/wandb:1.2.3" + ociRelease.Version = "" + + ref, err := ociRelease.pullReference(registryClient) + Expect(err).NotTo(HaveOccurred()) + Expect(ref).To(Equal("oci://ghcr.io/wandb/charts/wandb:1.2.3")) + }) + + It("should preserve an explicit digest from the URL", func() { + ociRelease.URL = "oci://ghcr.io/wandb/charts/wandb@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888" + ociRelease.Version = "" + + ref, err := ociRelease.pullReference(registryClient) + Expect(err).NotTo(HaveOccurred()) + Expect(ref).To(Equal("oci://ghcr.io/wandb/charts/wandb@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888")) + }) + + It("should combine a repository URL with the version field", func() { + ref, err := ociRelease.pullReference(registryClient) + Expect(err).NotTo(HaveOccurred()) + Expect(ref).To(Equal("oci://ghcr.io/wandb/helm-charts/operator-wandb:1.0.0")) + }) + + It("should reject mismatched tagged URLs and version fields", func() { + ociRelease.URL = "oci://ghcr.io/wandb/charts/wandb:1.2.3" + ociRelease.Version = "2.0.0" + + _, err := ociRelease.pullReference(registryClient) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("chart reference and version mismatch")) + }) + }) + + Describe("Apply", func() { + It("should return error when pullChart fails", func() { + ociRelease.URL = "oci://localhost:1/nonexistent/chart" + ociRelease.PlainHTTP = true + err := ociRelease.Apply(context.TODO(), nil, &v1.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + }, nil, nil) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("Prune", func() { + It("should return error when actionable chart creation fails", func() { + // Empty name will fail release name validation + err := ociRelease.Prune(context.TODO(), nil, &v1.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{ + Name: "", + Namespace: "default", + }, + }, nil, nil) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("CredentialSecret", func() { + var ( + fakeClient client.Client + scheme *runtime.Scheme + wandb *v1.WeightsAndBiases + config spec.Values + ) + + BeforeEach(func() { + scheme = runtime.NewScheme() + Expect(v1.AddToScheme(scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + + fakeClient = fake.NewClientBuilder().WithScheme(scheme).Build() + + wandb = &v1.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-wandb", + Namespace: "test-namespace", + }, + } + + config = spec.Values{} + + GinkgoT().Setenv("HELM_CACHE_HOME", filepath.Join(os.TempDir(), "oci-test-cache")) + GinkgoT().Setenv("HELM_CONFIG_HOME", filepath.Join(os.TempDir(), "oci-test-config")) + GinkgoT().Setenv("HELM_DATA_HOME", filepath.Join(os.TempDir(), "oci-test-data")) + }) + + Context("with valid credential secret", func() { + BeforeEach(func() { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-oci-credentials", + Namespace: "test-namespace", + }, + Data: map[string][]byte{ + "HELM_USERNAME": []byte("secret-user"), + "HELM_PASSWORD": []byte("secret-pass"), + }, + } + Expect(fakeClient.Create(context.TODO(), secret)).To(Succeed()) + + ociRelease.URL = "oci://localhost:1/nonexistent/chart" + ociRelease.PlainHTTP = true + ociRelease.CredentialSecret = &CredentialSecret{ + Name: "test-oci-credentials", + UsernameKey: "HELM_USERNAME", + PasswordKey: "HELM_PASSWORD", + } + ociRelease.Username = "" + ociRelease.Password = "" + }) + + It("should retrieve credentials from secret before pull", func() { + err := ociRelease.Apply(context.TODO(), fakeClient, wandb, scheme, config) + // Should fail at pull step, not credential retrieval + Expect(err).To(HaveOccurred()) + Expect(err.Error()).NotTo(ContainSubstring("Failed to get credentials from secret")) + }) + }) + + Context("with default credential keys", func() { + BeforeEach(func() { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-oci-defaults", + Namespace: "test-namespace", + }, + Data: map[string][]byte{ + "HELM_USERNAME": []byte("default-user"), + "HELM_PASSWORD": []byte("default-pass"), + }, + } + Expect(fakeClient.Create(context.TODO(), secret)).To(Succeed()) + + ociRelease.URL = "oci://localhost:1/nonexistent/chart" + ociRelease.PlainHTTP = true + ociRelease.CredentialSecret = &CredentialSecret{ + Name: "test-oci-defaults", + } + }) + + It("should use default credential keys", func() { + err := ociRelease.Apply(context.TODO(), fakeClient, wandb, scheme, config) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).NotTo(ContainSubstring("Failed to get credentials from secret")) + }) + }) + + Context("with missing credential secret", func() { + BeforeEach(func() { + ociRelease.URL = "oci://localhost:1/nonexistent/chart" + ociRelease.PlainHTTP = true + ociRelease.CredentialSecret = &CredentialSecret{ + Name: "non-existent-secret", + UsernameKey: "HELM_USERNAME", + PasswordKey: "HELM_PASSWORD", + } + }) + + It("should fail when secret does not exist", func() { + err := ociRelease.Apply(context.TODO(), fakeClient, wandb, scheme, config) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("non-existent-secret")) + }) + }) + + Context("without credential secret", func() { + BeforeEach(func() { + ociRelease.URL = "oci://localhost:1/nonexistent/chart" + ociRelease.PlainHTTP = true + ociRelease.CredentialSecret = nil + ociRelease.Username = "direct-user" + ociRelease.Password = "direct-pass" + }) + + It("should use direct credentials", func() { + err := ociRelease.Apply(context.TODO(), fakeClient, wandb, scheme, config) + // Should fail at pull step, not credential setup + Expect(err).To(HaveOccurred()) + Expect(err.Error()).NotTo(ContainSubstring("Failed to get credentials from secret")) + }) + }) + }) +}) diff --git a/pkg/wandb/spec/charts/repo.go b/pkg/wandb/spec/charts/repo.go index 2dc1df43..bf85e6ab 100644 --- a/pkg/wandb/spec/charts/repo.go +++ b/pkg/wandb/spec/charts/repo.go @@ -13,11 +13,12 @@ import ( v1 "github.com/wandb/operator/api/v1" "github.com/wandb/operator/pkg/helm" "github.com/wandb/operator/pkg/wandb/spec" - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/downloader" - "helm.sh/helm/v3/pkg/getter" - "helm.sh/helm/v3/pkg/repo" + chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/cli" + "helm.sh/helm/v4/pkg/downloader" + "helm.sh/helm/v4/pkg/getter" + "helm.sh/helm/v4/pkg/registry" + repo "helm.sh/helm/v4/pkg/repo/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" @@ -27,7 +28,7 @@ const CredentialUsernameKey = "HELM_USERNAME" const CredentialPasswordKey = "HELM_PASSWORD" type RepoRelease struct { - URL string `validate:"required,url" json:"url"` + URL string `validate:"required,url,nonociurl" json:"url"` Name string `validate:"required" json:"name"` // If version is not set, download latest. @@ -77,8 +78,14 @@ func (r RepoRelease) Chart() (*chart.Chart, error) { return local.Chart() } +func validateNonOCIURL(fl validator.FieldLevel) bool { + return !registry.IsOCI(fl.Field().String()) +} + func (c RepoRelease) Validate() error { - return validator.New().Struct(c) + v := validator.New() + v.RegisterValidation("nonociurl", validateNonOCIURL) + return v.Struct(c) } func (r RepoRelease) ToLocalRelease() (*LocalRelease, error) { @@ -164,8 +171,8 @@ func (r RepoRelease) downloadChart() (string, error) { return "", err } - entry.InsecureSkipTLSverify = parsedURL.Scheme == "http" - if entry.InsecureSkipTLSverify && r.Debug { + entry.InsecureSkipTLSVerify = parsedURL.Scheme == "http" + if entry.InsecureSkipTLSVerify && r.Debug { log.Info("TLS verification disabled for HTTP URL", "url", r.URL) } @@ -196,7 +203,7 @@ func (r RepoRelease) downloadChart() (string, error) { getterOpts := []getter.Option{ getter.WithBasicAuth(r.Username, r.Password), - getter.WithInsecureSkipVerifyTLS(true), + getter.WithInsecureSkipVerifyTLS(entry.InsecureSkipTLSVerify), } providers := getter.All(settings) @@ -273,6 +280,7 @@ func (r RepoRelease) downloadChart() (string, error) { RegistryClient: cfg.RegistryClient, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, + ContentCache: settings.ContentCache, } dest := filepath.Join(os.Getenv("HELM_DATA_HOME"), "charts") diff --git a/pkg/wandb/spec/charts/repo_test.go b/pkg/wandb/spec/charts/repo_test.go index 2778da4f..65ef8e75 100644 --- a/pkg/wandb/spec/charts/repo_test.go +++ b/pkg/wandb/spec/charts/repo_test.go @@ -16,8 +16,8 @@ import ( . "github.com/onsi/gomega" v1 "github.com/wandb/operator/api/v1" "github.com/wandb/operator/pkg/wandb/spec" - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/repo" + chart "helm.sh/helm/v4/pkg/chart/v2" + repo "helm.sh/helm/v4/pkg/repo/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/pkg/wandb/spec/config.go b/pkg/wandb/spec/config.go index 42e898ed..cca68907 100644 --- a/pkg/wandb/spec/config.go +++ b/pkg/wandb/spec/config.go @@ -7,8 +7,8 @@ import ( "github.com/pkg/errors" "github.com/wandb/operator/pkg/utils" - "helm.sh/helm/v3/pkg/chartutil" - "helm.sh/helm/v3/pkg/strvals" + chartutil "helm.sh/helm/v4/pkg/chart/common/util" + "helm.sh/helm/v4/pkg/strvals" ) // Values holds an arbitrary tree-like data structure, such as parsed JSON or diff --git a/pkg/wandb/spec/spec.go b/pkg/wandb/spec/spec.go index ae0dcf46..811c9de0 100644 --- a/pkg/wandb/spec/spec.go +++ b/pkg/wandb/spec/spec.go @@ -7,7 +7,7 @@ import ( "reflect" v1 "github.com/wandb/operator/api/v1" - "helm.sh/helm/v3/pkg/chart" + chart "helm.sh/helm/v4/pkg/chart/v2" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/pkg/wandb/spec/specfakes/fake_chart.go b/pkg/wandb/spec/specfakes/fake_chart.go index 6a6b4a68..b78ae2f8 100644 --- a/pkg/wandb/spec/specfakes/fake_chart.go +++ b/pkg/wandb/spec/specfakes/fake_chart.go @@ -7,7 +7,7 @@ import ( v1 "github.com/wandb/operator/api/v1" "github.com/wandb/operator/pkg/wandb/spec" - "helm.sh/helm/v3/pkg/chart" + v2 "helm.sh/helm/v4/pkg/chart/v2" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -28,16 +28,16 @@ type FakeChart struct { applyReturnsOnCall map[int]struct { result1 error } - ChartStub func() (*chart.Chart, error) + ChartStub func() (*v2.Chart, error) chartMutex sync.RWMutex chartArgsForCall []struct { } chartReturns struct { - result1 *chart.Chart + result1 *v2.Chart result2 error } chartReturnsOnCall map[int]struct { - result1 *chart.Chart + result1 *v2.Chart result2 error } PruneStub func(context.Context, client.Client, *v1.WeightsAndBiases, *runtime.Scheme, spec.Values) error @@ -124,7 +124,7 @@ func (fake *FakeChart) ApplyReturnsOnCall(i int, result1 error) { }{result1} } -func (fake *FakeChart) Chart() (*chart.Chart, error) { +func (fake *FakeChart) Chart() (*v2.Chart, error) { fake.chartMutex.Lock() ret, specificReturn := fake.chartReturnsOnCall[len(fake.chartArgsForCall)] fake.chartArgsForCall = append(fake.chartArgsForCall, struct { @@ -148,34 +148,34 @@ func (fake *FakeChart) ChartCallCount() int { return len(fake.chartArgsForCall) } -func (fake *FakeChart) ChartCalls(stub func() (*chart.Chart, error)) { +func (fake *FakeChart) ChartCalls(stub func() (*v2.Chart, error)) { fake.chartMutex.Lock() defer fake.chartMutex.Unlock() fake.ChartStub = stub } -func (fake *FakeChart) ChartReturns(result1 *chart.Chart, result2 error) { +func (fake *FakeChart) ChartReturns(result1 *v2.Chart, result2 error) { fake.chartMutex.Lock() defer fake.chartMutex.Unlock() fake.ChartStub = nil fake.chartReturns = struct { - result1 *chart.Chart + result1 *v2.Chart result2 error }{result1, result2} } -func (fake *FakeChart) ChartReturnsOnCall(i int, result1 *chart.Chart, result2 error) { +func (fake *FakeChart) ChartReturnsOnCall(i int, result1 *v2.Chart, result2 error) { fake.chartMutex.Lock() defer fake.chartMutex.Unlock() fake.ChartStub = nil if fake.chartReturnsOnCall == nil { fake.chartReturnsOnCall = make(map[int]struct { - result1 *chart.Chart + result1 *v2.Chart result2 error }) } fake.chartReturnsOnCall[i] = struct { - result1 *chart.Chart + result1 *v2.Chart result2 error }{result1, result2} } diff --git a/tilt-settings.sample.json b/tilt-settings.sample.json index 16d0d012..a9c794dc 100644 --- a/tilt-settings.sample.json +++ b/tilt-settings.sample.json @@ -1,5 +1,10 @@ { - "allowedContexts": ["docker-desktop", "minikube", "kind-kind"], + "allowedContexts": [ + "docker-desktop", + "minikube", + "kind-kind", + "orbstack" + ], "installMinio": true, "installWandb": true, "wandbCRD": "default" From 899dd45d920204c3e2269f4c10a3994103a6fae8 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 30 Apr 2026 20:40:02 +0000 Subject: [PATCH 09/49] chore(release): version 1.22.0 [skip ci] ## [1.22.0](https://github.com/wandb/operator/compare/v1.21.3...v1.22.0) (2026-04-30) ### Features * Add OCI Helm chart registry support and upgrade to Helm v4 ([#147](https://github.com/wandb/operator/issues/147)) ([fcb72b8](https://github.com/wandb/operator/commit/fcb72b8652c6370fb05aa6a805707540c1f57d81)) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d076425..e730dc9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## [1.22.0](https://github.com/wandb/operator/compare/v1.21.3...v1.22.0) (2026-04-30) + + +### Features + +* Add OCI Helm chart registry support and upgrade to Helm v4 ([#147](https://github.com/wandb/operator/issues/147)) ([fcb72b8](https://github.com/wandb/operator/commit/fcb72b8652c6370fb05aa6a805707540c1f57d81)) + ### [1.21.3](https://github.com/wandb/operator/compare/v1.21.2...v1.21.3) (2025-12-04) From 9754b3a78b069c11570800d69dc38c336b171bc4 Mon Sep 17 00:00:00 2001 From: shivawandb <167111773+shivawandb@users.noreply.github.com> Date: Fri, 26 Jun 2026 09:13:59 -0700 Subject: [PATCH 10/49] chore(security): move GitHub Actions updates to Renovate (#222) --- .github/dependabot.yml | 6 ------ .github/renovate.json5 | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) create mode 100644 .github/renovate.json5 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a16e42c1..022c83d6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,9 +14,3 @@ updates: reviewers: - wandb/delivery-tooling-team - - package-ecosystem: github-actions - directory: /.github/workflows - schedule: - interval: daily - reviewers: - - wandb/delivery-tooling-team diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 00000000..94cee6c7 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,4 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["github>wandb/renovate-config"] +} From ff42719fc25f6eadcffe0e483112c3ed5a217e46 Mon Sep 17 00:00:00 2001 From: "wandb-renovate[bot]" <285832833+wandb-renovate[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 07:56:29 -0700 Subject: [PATCH 11/49] chore(deps): pin dependencies (#226) Co-authored-by: wandb-renovate[bot] <285832833+wandb-renovate[bot]@users.noreply.github.com> --- .github/workflows/docker-build-scan.yml | 6 +++--- .github/workflows/internal-chart-publish.yaml | 2 +- .github/workflows/internal-image-publish.yaml | 2 +- .github/workflows/pr-title.yaml | 2 +- .github/workflows/release.yaml | 12 ++++++------ .github/workflows/run-tests.yaml | 12 ++++++------ 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/docker-build-scan.yml b/.github/workflows/docker-build-scan.yml index e5908770..2397b11c 100644 --- a/.github/workflows/docker-build-scan.yml +++ b/.github/workflows/docker-build-scan.yml @@ -11,13 +11,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Build Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 with: context: . push: false diff --git a/.github/workflows/internal-chart-publish.yaml b/.github/workflows/internal-chart-publish.yaml index 569c000e..195bc03c 100644 --- a/.github/workflows/internal-chart-publish.yaml +++ b/.github/workflows/internal-chart-publish.yaml @@ -12,7 +12,7 @@ jobs: id-token: 'write' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/internal-image-publish.yaml b/.github/workflows/internal-image-publish.yaml index 06b1886c..8b15dc86 100644 --- a/.github/workflows/internal-image-publish.yaml +++ b/.github/workflows/internal-image-publish.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 with: persist-credentials: false fetch-depth: 0 diff --git a/.github/workflows/pr-title.yaml b/.github/workflows/pr-title.yaml index b9437ecb..15705a24 100644 --- a/.github/workflows/pr-title.yaml +++ b/.github/workflows/pr-title.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: # https://github.com/amannn/action-semantic-pull-request/releases - - uses: amannn/action-semantic-pull-request@v4.2.0 + - uses: amannn/action-semantic-pull-request@0eb081bc9c35210408951834a444794406eff6f8 # v4.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 42b355b8..cb715c8d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -14,24 +14,24 @@ jobs: if: github.repository_owner == 'wandb' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 with: persist-credentials: false fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - name: Login to quay.io - uses: docker/login-action@v2 + uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2 with: username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_TOKEN }} @@ -49,7 +49,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GH_SECRET }} - - uses: actions/setup-go@v4 + - uses: actions/setup-go@7b8cf10d4e4a01d4992d18a89f4d7dc5a3e6d6f4 # v4 with: go-version: 1.25 diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 7c4f7938..d10c97ee 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -11,9 +11,9 @@ jobs: name: Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version: 1.25 - name: Install dependencies @@ -27,9 +27,9 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version: 1.25 - name: Build @@ -39,9 +39,9 @@ jobs: name: Dependency Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version: 1.25 - name: Install dependencies From 726dfe684047117743b9bce2405ad85901ed569a Mon Sep 17 00:00:00 2001 From: Will Nevis Date: Wed, 8 Jul 2026 15:55:25 -0700 Subject: [PATCH 12/49] Add standard labels to all pods, documentation --- internal/controller/application_controller.go | 52 ++++++++++++++ internal/controller/common/labels.go | 72 +++++++++++++++++++ .../infra/managed/clickhouse/altinity/spec.go | 5 +- .../infra/managed/kafka/bufstream/spec.go | 8 ++- .../infra/managed/mysql/moco/spec.go | 6 ++ internal/controller/reconciler/mysql.go | 8 +-- internal/controller/reconciler/rbac.go | 4 ++ .../controller/reconciler/reconcile_v2.go | 27 ++++--- 8 files changed, 164 insertions(+), 18 deletions(-) diff --git a/internal/controller/application_controller.go b/internal/controller/application_controller.go index 1d4dbbc0..c9e453b6 100644 --- a/internal/controller/application_controller.go +++ b/internal/controller/application_controller.go @@ -23,6 +23,7 @@ import ( gkeGatewayApiNetworkingv1 "github.com/GoogleCloudPlatform/gke-gateway-api/apis/networking/v1" wandbv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" "github.com/wandb/operator/internal/logx" "github.com/wandb/operator/pkg/utils" v1alpha1 "github.com/wandb/operator/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1" @@ -257,6 +258,16 @@ func (r *ApplicationReconciler) reconcileDeployment(ctx context.Context, app *wa selectorLabels := getSelectorLabels(app) + // spec.selector is immutable. If it drifted (e.g. label-standard migration), + // delete the existing Deployment and recreate it on the next reconcile. + if !deployment.CreationTimestamp.IsZero() && selectorChanged(deployment.Spec.Selector, selectorLabels) { + logger.Info("Deployment selector changed; deleting for recreate", "Deployment", app.Name) + if err := r.Delete(ctx, deployment, client.PropagationPolicy(metav1.DeletePropagationForeground)); err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + deployment.Name = app.Name deployment.Namespace = app.Namespace @@ -313,13 +324,38 @@ func (r *ApplicationReconciler) reconcileDeployment(ctx context.Context, app *wa return ctrl.Result{}, nil } +// getSelectorLabels returns the immutable label set used for a workload's +// spec.selector. It derives the selector from the operator/ownership family +// (weightsandbiases.apps.wandb.com/*) stamped onto the pod template by the +// WeightsAndBiases reconciler, which is stable and collision-free. Applications +// created before that family existed fall back to the legacy app.kubernetes.io +// selector so their live workloads keep matching. func getSelectorLabels(app *wandbv2.Application) map[string]string { + podLabels := app.Spec.PodTemplate.GetLabels() + if name, ok := podLabels[common.WandbNameLabel]; ok && name != "" { + selector := map[string]string{common.WandbNameLabel: name} + if component, ok := podLabels[common.WandbComponentLabel]; ok && component != "" { + selector[common.WandbComponentLabel] = component + } + return selector + } + // Legacy fallback (pre-operator-family Applications). return map[string]string{ "app.kubernetes.io/name": app.Name, "app.kubernetes.io/instance": app.Namespace, } } +// recreateOnSelectorChange deletes a workload whose immutable spec.selector no +// longer matches the desired selector so it can be recreated on the next +// reconcile. Returns true when a delete was issued (caller should requeue). +func selectorChanged(current *metav1.LabelSelector, desired map[string]string) bool { + if current == nil { + return false + } + return !reflect.DeepEqual(current.MatchLabels, desired) +} + // deleteDeployment deletes the Deployment associated with the Application func (r *ApplicationReconciler) deleteDeployment(ctx context.Context, app *wandbv2.Application) error { logger := logx.GetSlog(ctx) @@ -363,6 +399,14 @@ func (r *ApplicationReconciler) reconcileRollout(ctx context.Context, app *wandb selectorLabels := getSelectorLabels(app) + if !rollout.CreationTimestamp.IsZero() && selectorChanged(rollout.Spec.Selector, selectorLabels) { + logger.Info("Rollout selector changed; deleting for recreate", "Rollout", app.Name) + if err := r.Delete(ctx, rollout, client.PropagationPolicy(metav1.DeletePropagationForeground)); err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + rollout.Name = app.Name rollout.Namespace = app.Namespace @@ -462,6 +506,14 @@ func (r *ApplicationReconciler) reconcileStatefulSet(ctx context.Context, app *w selectorLabels := getSelectorLabels(app) + if !statefulSet.CreationTimestamp.IsZero() && selectorChanged(statefulSet.Spec.Selector, selectorLabels) { + logger.Info("StatefulSet selector changed; deleting for recreate", "StatefulSet", app.Name) + if err := r.Delete(ctx, statefulSet, client.PropagationPolicy(metav1.DeletePropagationForeground)); err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + statefulSet.Name = app.Name statefulSet.Namespace = app.Namespace diff --git a/internal/controller/common/labels.go b/internal/controller/common/labels.go index 245a830b..218870c0 100644 --- a/internal/controller/common/labels.go +++ b/internal/controller/common/labels.go @@ -10,6 +10,78 @@ const ( WandbComponentLabel = "weightsandbiases.apps.wandb.com/component" ) +// Standard Kubernetes "recommended" label keys. These are descriptive labels for +// ecosystem tooling (kube-state-metrics, dashboards, kubectl) and NetworkPolicy +// selectors. They are intentionally distinct from the operator/ownership family +// above, which backs immutable spec.selectors and retention selectors. +const ( + StandardNameLabel = "app.kubernetes.io/name" + StandardInstanceLabel = "app.kubernetes.io/instance" + StandardComponentLabel = "app.kubernetes.io/component" + StandardPartOfLabel = "app.kubernetes.io/part-of" + StandardManagedByLabel = "app.kubernetes.io/managed-by" + StandardVersionLabel = "app.kubernetes.io/version" + + // PartOfValue is the value shared by every wandb-managed resource; it is the + // anchor for namespace-wide NetworkPolicies and release-wide queries. + PartOfValue = "wandb" + // ManagedByValue identifies resources reconciled by this operator. + ManagedByValue = "wandb-operator" +) + +// Architectural component roles. See docs/pod-labeling-standards.md. +const ( + RoleServer = "server" + RoleWorker = "worker" + RoleProxy = "proxy" + RoleDatabase = "database" + RoleCache = "cache" + RoleAnalyticsDB = "analytics-db" + RoleQueue = "queue" + RoleObjectStorage = "object-storage" + RoleMigration = "migration" +) + +// appComponentRoles maps known W&B application (manifest) names to their +// architectural role. Unknown apps default to RoleServer. +var appComponentRoles = map[string]string{ + "executor": RoleWorker, + "parquet": RoleWorker, + "weave-trace-worker": RoleWorker, + "weave-trace-evaluate-model-worker": RoleWorker, + "flat-run-fields-updater": RoleWorker, + "metric-observer": RoleWorker, + "nginx-proxy": RoleProxy, +} + +// AppComponentRole returns the architectural role for a W&B application name, +// defaulting to RoleServer for request-serving apps. +func AppComponentRole(appName string) string { + if role, ok := appComponentRoles[appName]; ok { + return role + } + return RoleServer +} + +// StandardLabels returns the descriptive app.kubernetes.io/* label set for a +// resource owned by the given CR. component and version are optional; empty +// values are omitted. instance is always the owning CR (release) name. +func StandardLabels(wandb *apiv2.WeightsAndBiases, name, component, version string) map[string]string { + l := map[string]string{ + StandardNameLabel: name, + StandardInstanceLabel: wandb.Name, + StandardPartOfLabel: PartOfValue, + StandardManagedByLabel: ManagedByValue, + } + if component != "" { + l[StandardComponentLabel] = component + } + if version != "" { + l[StandardVersionLabel] = version + } + return l +} + // HasAllLabelKeys reports whether existing contains every key present in desired, // regardless of value. func HasAllLabelKeys(existing, desired map[string]string) bool { diff --git a/internal/controller/infra/managed/clickhouse/altinity/spec.go b/internal/controller/infra/managed/clickhouse/altinity/spec.go index 6300763c..b3edc0dc 100644 --- a/internal/controller/infra/managed/clickhouse/altinity/spec.go +++ b/internal/controller/infra/managed/clickhouse/altinity/spec.go @@ -225,7 +225,10 @@ func ToClickHouseVendorSpec( { Name: nsnBuilder.PodTemplateName(), ObjectMeta: metav1.ObjectMeta{ - Labels: BuildWandbClickhouseLabels(wandb), + Labels: utils.MergeMapsStringString( + BuildWandbClickhouseLabels(wandb), + common.StandardLabels(wandb, "clickhouse", common.RoleAnalyticsDB, ""), + ), }, Spec: podSpec, }, diff --git a/internal/controller/infra/managed/kafka/bufstream/spec.go b/internal/controller/infra/managed/kafka/bufstream/spec.go index 920af7cc..cf96bfd4 100644 --- a/internal/controller/infra/managed/kafka/bufstream/spec.go +++ b/internal/controller/infra/managed/kafka/bufstream/spec.go @@ -244,7 +244,9 @@ func ToEtcdApplication( Replicas: ptr.To(int32(EtcdReplicas)), ServiceName: nsnBuilder.EtcdName(), MetaTemplate: metav1.ObjectMeta{ - Labels: labels, + Labels: utils.MergeMapsStringString( + labels, common.StandardLabels(wandb, "etcd", common.RoleDatabase, ""), + ), }, PodTemplate: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ @@ -489,7 +491,9 @@ func ToBufstreamApplication( Kind: "Deployment", Replicas: ptr.To(replicas), MetaTemplate: metav1.ObjectMeta{ - Labels: labels, + Labels: utils.MergeMapsStringString( + labels, common.StandardLabels(wandb, "kafka", common.RoleQueue, ""), + ), }, PodTemplate: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ diff --git a/internal/controller/infra/managed/mysql/moco/spec.go b/internal/controller/infra/managed/mysql/moco/spec.go index 89c88c48..79612a93 100644 --- a/internal/controller/infra/managed/mysql/moco/spec.go +++ b/internal/controller/infra/managed/mysql/moco/spec.go @@ -80,6 +80,12 @@ func ToMocoMySQLClusterSpec( Replicas: replicas, MySQLConfigMapName: ptr.To(MyCnfConfigMapName(spec.Name)), PodTemplate: mocov1beta2.PodTemplateSpec{ + ObjectMeta: mocov1beta2.ObjectMeta{ + Labels: utils.MergeMapsStringString( + BuildWandbMysqlLabels(wandb), + common.StandardLabels(wandb, "mysql", common.RoleDatabase, ""), + ), + }, Spec: buildMocoPodSpec(spec.Config.Resources, mfst.Mysql["default"].Images["mysql"], wandb), OverwriteContainers: mocoOverwriteContainers(), }, diff --git a/internal/controller/reconciler/mysql.go b/internal/controller/reconciler/mysql.go index 7b4a09a9..2d62512f 100644 --- a/internal/controller/reconciler/mysql.go +++ b/internal/controller/reconciler/mysql.go @@ -309,18 +309,16 @@ func runMysqlInitJob(ctx context.Context, client client.Client, wandb *apiv2.Wei } } + initJobLabels := common.StandardLabels(wandb, "moco-init", common.RoleMigration, "") job = &v1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: jobName, Namespace: wandb.Namespace, - Labels: map[string]string{ - "app.kubernetes.io/managed-by": "wandb-operator", - "app.kubernetes.io/instance": wandb.Name, - "app.kubernetes.io/component": "moco-init", - }, + Labels: initJobLabels, }, Spec: v1.JobSpec{ Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: initJobLabels}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyOnFailure, Containers: []corev1.Container{ diff --git a/internal/controller/reconciler/rbac.go b/internal/controller/reconciler/rbac.go index 66dcd298..4196869b 100644 --- a/internal/controller/reconciler/rbac.go +++ b/internal/controller/reconciler/rbac.go @@ -29,10 +29,14 @@ func createOrUpdateServiceAccount( ObjectMeta: v3.ObjectMeta{ Name: serviceAccountName, Namespace: wandb.Namespace, + // The shared ServiceAccount spans all services, so it is exempt from + // name/component per docs/pod-labeling-standards.md; it still carries + // the descriptive identity labels. Labels: map[string]string{ "app.kubernetes.io/managed-by": "wandb-operator", "app.kubernetes.io/instance": wandb.Name, "app.kubernetes.io/part-of": "wandb", + "app.kubernetes.io/version": wandb.Spec.Wandb.Version, }, Annotations: wandb.Spec.Wandb.ServiceAccount.Annotations, }, diff --git a/internal/controller/reconciler/reconcile_v2.go b/internal/controller/reconciler/reconcile_v2.go index 7737443b..a0251937 100644 --- a/internal/controller/reconciler/reconcile_v2.go +++ b/internal/controller/reconciler/reconcile_v2.go @@ -604,6 +604,17 @@ func reconcileApplications( } application.Spec.Kind = "Deployment" + + // Label the workload with both families: the descriptive app.kubernetes.io/* + // set (for tooling/NetworkPolicies) and the operator/ownership set (which + // backs the immutable pod selector). See docs/pod-labeling-standards.md. + standardLabels := common.StandardLabels(wandb, app.Name, common.AppComponentRole(app.Name), wandb.Spec.Wandb.Version) + operatorLabels := common.BuildWandbLabels(wandb, app.Name) + application.Spec.MetaTemplate.Labels = oputils.MergeMapsStringString(application.Spec.MetaTemplate.Labels, standardLabels) + application.Spec.PodTemplate.Labels = oputils.MergeMapsStringString( + application.Spec.PodTemplate.Labels, standardLabels, operatorLabels, + ) + application.Spec.PodTemplate.Spec.Containers = containers // Replace volumes entirely on each reconcile to avoid accumulating duplicates // across updates (e.g., duplicate "files-inline" volume names). @@ -1194,18 +1205,16 @@ func runMigrations(ctx context.Context, client ctrlClient.Client, wandb *apiv2.W return ctrl.Result{}, err } + migrationJobLabels := common.StandardLabels(wandb, "migration", common.RoleMigration, wandb.Spec.Wandb.Version) job = &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: jobName, Namespace: wandb.Namespace, - Labels: map[string]string{ - "app.kubernetes.io/managed-by": "wandb-operator", - "app.kubernetes.io/instance": wandb.Name, - "app.kubernetes.io/component": "migration", - }, + Labels: migrationJobLabels, }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: migrationJobLabels}, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyOnFailure, Containers: []corev1.Container{ @@ -1326,11 +1335,9 @@ func generateSecrets(ctx context.Context, client ctrlClient.Client, wandb *apiv2 ObjectMeta: metav1.ObjectMeta{ Name: secretName, Namespace: wandb.Namespace, - Labels: map[string]string{ - "app.kubernetes.io/managed-by": "wandb-operator", - "app.kubernetes.io/instance": wandb.Name, - "app.kubernetes.io/part-of": "wandb", - }, + // Generated secrets are role-less, so component is omitted + // per docs/pod-labeling-standards.md (non-pod resources). + Labels: common.StandardLabels(wandb, gs.Name, "", ""), }, StringData: map[string]string{keyName: pw}, Type: corev1.SecretTypeOpaque, From e3c168ed4642f1db8e9a328f8b07b4e38d5f59ce Mon Sep 17 00:00:00 2001 From: Will Nevis Date: Wed, 8 Jul 2026 15:55:39 -0700 Subject: [PATCH 13/49] Add docs, tests for labels --- docs/pod-labeling-standards.md | 271 ++++++++++++++++++++++ internal/controller/common/labels_test.go | 41 ++++ 2 files changed, 312 insertions(+) create mode 100644 docs/pod-labeling-standards.md create mode 100644 internal/controller/common/labels_test.go diff --git a/docs/pod-labeling-standards.md b/docs/pod-labeling-standards.md new file mode 100644 index 00000000..1146e826 --- /dev/null +++ b/docs/pod-labeling-standards.md @@ -0,0 +1,271 @@ +# Pod Labeling Standards + +This document defines the labeling standards for all pods (and their controlling +workloads) that the W&B operator creates or manages, with a dedicated section for +non-pod resources such as ServiceAccounts. It is the contract that NetworkPolicies, +dashboards, metrics, and `kubectl` selectors depend on, so treat these labels as a +stable, public API. + +## Goals + +- Every operator-managed pod is identifiable by a **single, consistent** set of labels. +- Standard ecosystem tooling (kube-state-metrics, Grafana, Lens, k9s, ArgoCD, + `kubectl`) works out of the box. +- The operator has **stable, immutable, collision-free** selectors for its own + ownership, pruning, and retention logic. +- Users can write portable NetworkPolicies and selectors against a documented label. + +## The two label families + +We deliberately maintain **two** label families, each with a distinct job. Do not +collapse them into one. + +| Family | Prefix | Purpose | Mutable? | +|--------|--------|---------|----------| +| **Standard / descriptive** | `app.kubernetes.io/*` | Interop with ecosystem tooling; human legibility | Yes — informational only | +| **Operator / ownership** | `weightsandbiases.apps.wandb.com/*` | Workload `spec.selector` and the operator's own list/match/retention logic | No — immutable selector anchor | + +**Rule of thumb:** if a human or third-party tool reads it, it's `app.kubernetes.io/*`. +If the operator matches on it or it backs an immutable `spec.selector`, it's +`weightsandbiases.apps.wandb.com/*`. Both families appear on every managed pod. + +## Standard labels (`app.kubernetes.io/*`) + +Apply all of the following to every operator-managed pod template. + +| Label | Value | Example | +|-------|-------|---------| +| `app.kubernetes.io/name` | The **software/service** that runs in the pod | `api`, `executor`, `mysql`, `weave-trace` | +| `app.kubernetes.io/instance` | The **owning `WeightsAndBiases` CR name** (the release) | `wandb` | +| `app.kubernetes.io/component` | The **architectural role** the workload plays | `server`, `worker`, `proxy`, `database`, `cache` | +| `app.kubernetes.io/part-of` | Always `wandb` | `wandb` | +| `app.kubernetes.io/managed-by` | Always `wandb-operator` | `wandb-operator` | +| `app.kubernetes.io/version` | W&B server version (optional but recommended) | `0.79.0` | + +### `name` vs `component` + +These are different axes and MUST NOT be treated as synonyms: + +- `name` answers *"what software is this?"* — the service/binary/image (`api`, + `mysql`, `redis`). +- `component` answers *"what role does it play?"* — its place in the architecture + (`server`, `database`, `cache`, `worker`). + +They coincide only in the degenerate case of a standalone app that is its own single +role. They diverge whenever the software name differs from its role (`mysql` → +`database`), when one binary runs in multiple roles (`weave-trace` server vs worker), +or when you want tier-level grouping (`component: server` matches every stateless web +service at once). Use `name` for per-service targeting and `component` for per-tier +targeting. + +### Rules + +- `app.kubernetes.io/part-of: wandb` MUST be present on **every** managed pod. It + is the single anchor that matches the entire deployment and the documented + NetworkPolicy selector. +- `app.kubernetes.io/instance` MUST be the CR/release name, **not** the namespace. + (This corrects the current app-pod behavior where `instance` is set to the + namespace.) +- `app.kubernetes.io/name` MUST come from the service vocabulary and + `app.kubernetes.io/component` from the role vocabulary (see the Vocabularies + section). No free-form values. +- These labels are **descriptive**. Never use them as a `Deployment`/`StatefulSet` + `spec.selector`, because Helm and users routinely override them and selectors are + immutable. + +## Operator labels (`weightsandbiases.apps.wandb.com/*`) + +These back the immutable `spec.selector` and the operator's ownership queries. Keep +the set **minimal and low-cardinality**. + +| Label | Value | +|-------|-------| +| `weightsandbiases.apps.wandb.com/name` | The owning CR name | +| `weightsandbiases.apps.wandb.com/namespace` | The owning CR namespace | +| `weightsandbiases.apps.wandb.com/component` | The **service identity** (equivalent to `app.kubernetes.io/name`, e.g. `mysql`) | + +### Naming caveat + +The operator family's `/component` key does **not** hold the role-based value from +`app.kubernetes.io/component`. For historical reasons (`common.BuildWandbLabels` +populates it from the module/service name), it carries the **service identity** — +i.e. it lines up with `app.kubernetes.io/name`, not the role. This is intentional: +the selector needs to be unique *per service*, and the service name is the stable, +collision-free key for that. Do not "fix" this to match the role vocabulary; doing +so would change immutable selectors. + +### Rules + +- These are produced by `common.BuildWandbLabels(wandb, component)` — use that + helper, do not hand-roll the keys. +- The subset used in a workload `spec.selector` is **immutable**. Once a workload + exists you cannot change its selector; altering it requires deleting and + recreating the workload. Treat any change here as a breaking migration. +- Never put high-cardinality or user-mutable values here. + +## Non-pod resources + +This standard is written for **workload pods**, but the operator also creates +ServiceAccounts, Services, Roles/RoleBindings, Secrets, and ConfigMaps. Apply the +labels to those as follows. + +- **Descriptive identity labels — apply everywhere.** Every operator-created object + MUST carry `app.kubernetes.io/part-of: wandb`, + `app.kubernetes.io/managed-by: wandb-operator`, + `app.kubernetes.io/instance: `, and (where known) + `app.kubernetes.io/version`. This keeps the whole release queryable and + attributable regardless of resource kind. + +- **`name` / `component` role model — pods only, with judgement for others.** The + service/role split is meaningful for a workload that runs a specific service in a + specific role. For a resource dedicated to one service (e.g. that service's own + `Service` object), set `name`/`component` to match its pods. For shared or + role-less resources, omit them rather than inventing a value. + +- **Operator / selector family — pods and their workloads only.** The + `weightsandbiases.apps.wandb.com/*` family exists to back immutable `spec.selector` + fields and pod-ownership queries. Non-pod resources have no `spec.selector`, so it + is not required on them (the operator already tracks them via owner references). + +### The shared ServiceAccount + +There is a single ServiceAccount (default `wandb`) **shared by every application +pod**, so it has no single service identity or architectural role. It is explicitly +**exempt from `name` and `component`**. It MUST still carry the descriptive identity +labels (`part-of`, `managed-by`, `instance`, and `version` when available), and user +annotations continue to flow from `spec.wandb.serviceAccount.annotations`. + +## Vocabularies + +Two closed vocabularies feed the labels above. If a new workload type is introduced, +extend both lists in the same PR. + +### Service names (`app.kubernetes.io/name`) + +The service/software identity. Sourced from the manifest application name or infra +module name. + +- Applications: `api`, `executor`, `filestream`, `filemeta`, `glue`, `parquet`, + `weave`, `weave-trace`, `weave-trace-worker`, `nginx-proxy`, + `flat-run-fields-updater`, `metric-observer`. +- Infrastructure: `mysql`, `redis`, `clickhouse`, `kafka`, `seaweedfs`. +- Operational: `migration`. + +### Component roles (`app.kubernetes.io/component`) + +The architectural role. Keep this list small and generic. + +- `server` — stateless request-serving apps. +- `worker` — async/background processors. +- `proxy` — ingress/edge proxies. +- `database` — relational stores. +- `cache` — in-memory caches. +- `analytics-db` — columnar/analytics stores. +- `queue` — message/streaming brokers. +- `object-storage` — blob/object stores. +- `migration` — one-shot migration/init jobs. + +### Mapping + +| Workload | `name` | `component` | +|----------|--------|-------------| +| api | `api` | `server` | +| executor | `executor` | `worker` | +| filestream | `filestream` | `server` | +| parquet | `parquet` | `worker` | +| weave-trace | `weave-trace` | `server` | +| weave-trace-worker | `weave-trace` | `worker` | +| nginx-proxy | `nginx-proxy` | `proxy` | +| MySQL | `mysql` | `database` | +| Redis | `redis` | `cache` | +| ClickHouse | `clickhouse` | `analytics-db` | +| Kafka | `kafka` | `queue` | +| SeaweedFS | `seaweedfs` | `object-storage` | +| migration/init job | `migration` | `migration` | + +Note that `name` and `component` differ for most workloads. They coincide only where +the service *is* its own single role (e.g. the `migration` job). + +## Worked example + +A pod for the `api` application in a CR named `wandb`, running server version +`0.79.0`, should carry: + +```yaml +metadata: + labels: + # Standard / descriptive + app.kubernetes.io/name: api # the service + app.kubernetes.io/instance: wandb + app.kubernetes.io/component: server # the role + app.kubernetes.io/part-of: wandb + app.kubernetes.io/managed-by: wandb-operator + app.kubernetes.io/version: 0.79.0 + # Operator / ownership (selector anchor) + weightsandbiases.apps.wandb.com/name: wandb + weightsandbiases.apps.wandb.com/namespace: wandb-system + weightsandbiases.apps.wandb.com/component: api # service identity (see Naming caveat) +``` + +The workload `spec.selector` matches only on the operator family, e.g.: + +```yaml +spec: + selector: + matchLabels: + weightsandbiases.apps.wandb.com/name: wandb + weightsandbiases.apps.wandb.com/component: api +``` + +## Using the labels + +### NetworkPolicies + +Document `app.kubernetes.io/part-of: wandb` as the anchor for the whole deployment. +Use `app.kubernetes.io/name` to target a **specific service** and +`app.kubernetes.io/component` to target a **whole tier** (e.g. every `database`). A +`NetworkPolicy` `podSelector` is independent of the workload's immutable +`spec.selector`, so it is safe to select on the descriptive labels here. + +```yaml +# Restrict the MySQL service's ingress to W&B pods only +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: wandb-mysql-restrict +spec: + podSelector: + matchLabels: + app.kubernetes.io/part-of: wandb + app.kubernetes.io/name: mysql # this specific service + policyTypes: [Ingress] + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/part-of: wandb + ports: + - { protocol: TCP, port: 3306 } +``` + +To apply a rule to every storage tier at once, select on the role instead — e.g. +`app.kubernetes.io/component: database` for all relational stores. + +### Metrics and dashboards + +kube-state-metrics exposes `app.kubernetes.io/*` as metric labels. Scope to a release +with `app.kubernetes.io/instance`, break down a single service with +`app.kubernetes.io/name`, and roll up a tier with `app.kubernetes.io/component`. + +### Ad-hoc queries + +```bash +# Everything in a release +kubectl get pods -l app.kubernetes.io/part-of=wandb,app.kubernetes.io/instance=wandb + +# One specific service +kubectl get pods -l app.kubernetes.io/name=clickhouse + +# A whole tier (all databases) +kubectl get pods -l app.kubernetes.io/component=database +``` diff --git a/internal/controller/common/labels_test.go b/internal/controller/common/labels_test.go new file mode 100644 index 00000000..2eadc614 --- /dev/null +++ b/internal/controller/common/labels_test.go @@ -0,0 +1,41 @@ +package common + +import ( + apiv2 "github.com/wandb/operator/api/v2" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("StandardLabels", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "wandb-system"}, + } + + It("sets instance to the CR name, not the namespace", func() { + labels := StandardLabels(wandb, "api", RoleServer, "0.80.0") + Expect(labels[StandardInstanceLabel]).To(Equal("wandb")) + Expect(labels[StandardNameLabel]).To(Equal("api")) + Expect(labels[StandardComponentLabel]).To(Equal(RoleServer)) + Expect(labels[StandardPartOfLabel]).To(Equal(PartOfValue)) + Expect(labels[StandardManagedByLabel]).To(Equal(ManagedByValue)) + Expect(labels[StandardVersionLabel]).To(Equal("0.80.0")) + }) + + It("omits component and version when empty", func() { + labels := StandardLabels(wandb, "generated-secret", "", "") + Expect(labels).NotTo(HaveKey(StandardComponentLabel)) + Expect(labels).NotTo(HaveKey(StandardVersionLabel)) + Expect(labels).To(HaveKeyWithValue(StandardPartOfLabel, PartOfValue)) + }) +}) + +var _ = Describe("AppComponentRole", func() { + It("maps known workers and proxies, defaulting others to server", func() { + Expect(AppComponentRole("executor")).To(Equal(RoleWorker)) + Expect(AppComponentRole("parquet")).To(Equal(RoleWorker)) + Expect(AppComponentRole("nginx-proxy")).To(Equal(RoleProxy)) + Expect(AppComponentRole("api")).To(Equal(RoleServer)) + Expect(AppComponentRole("some-new-service")).To(Equal(RoleServer)) + }) +}) From f19b8e1401026f946bed2a69167ff607e8a19b94 Mon Sep 17 00:00:00 2001 From: Jagat Thakkar <32109558+jthakkar04@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:04:27 -0500 Subject: [PATCH 14/49] chore: Update CODEOWNERS to on-prem-team (#210) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 49013d13..9287de11 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @wandb/delivery-tooling-team \ No newline at end of file +* @wandb/on-prem-team \ No newline at end of file From b02613ecf1f4c8d3626b6b76e381989f9d11e0a9 Mon Sep 17 00:00:00 2001 From: Casey Abernathy Date: Wed, 15 Jul 2026 09:48:33 -0700 Subject: [PATCH 15/49] feat: Prep main for v2 merge (#251) --- .github/workflows/docker-build-scan.yml | 4 +- .github/workflows/release.yaml | 153 ++++++++++++++---------- .github/workflows/run-tests.yaml | 4 +- .releaserc.json | 33 ----- docs/releasing-v1.md | 35 ++++++ 5 files changed, 131 insertions(+), 98 deletions(-) delete mode 100644 .releaserc.json create mode 100644 docs/releasing-v1.md diff --git a/.github/workflows/docker-build-scan.yml b/.github/workflows/docker-build-scan.yml index 2397b11c..4dd26199 100644 --- a/.github/workflows/docker-build-scan.yml +++ b/.github/workflows/docker-build-scan.yml @@ -2,9 +2,9 @@ name: Docker Build and Security Scan on: push: - branches: [main] + branches: [v1] pull_request: - branches: [main] + branches: [v1] jobs: build-and-scan: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index cb715c8d..7e232d64 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,91 +1,122 @@ -name: Release +name: Release v1 on: - workflow_dispatch: push: - branches: - - main + tags: + - "v1.*.*" + +permissions: + contents: write + +concurrency: + group: release-v1 + cancel-in-progress: false jobs: release: - name: Release - runs-on: ubuntu-latest - # Skip running release workflow on forks + name: Release v1 if: github.repository_owner == 'wandb' + runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 + - name: Checkout release tag + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - persist-credentials: false fetch-depth: 0 + persist-credentials: false + + - name: Validate release tag + id: release + shell: bash + run: | + set -euo pipefail + + tag="${GITHUB_REF_NAME}" + if [[ ! "${tag}" =~ ^v(1)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "Expected a stable v1 tag in the form v1.x.y; got ${tag}" >&2 + exit 1 + fi + + if [[ "$(git cat-file -t "refs/tags/${tag}")" != "tag" ]]; then + echo "Release tag ${tag} must be annotated" >&2 + exit 1 + fi + + git fetch --no-tags origin refs/heads/v1:refs/remotes/origin/v1 + tagged_commit="$(git rev-list -n 1 "${tag}")" + if ! git merge-base --is-ancestor "${tagged_commit}" refs/remotes/origin/v1; then + echo "Tagged commit ${tagged_commit} is not reachable from origin/v1" >&2 + exit 1 + fi + + version="${tag#v}" + minor="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" + major="${BASH_REMATCH[1]}" - - name: Set up QEMU - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2 + echo "tag=${tag}" >> "${GITHUB_OUTPUT}" + echo "tagged_commit=${tagged_commit}" >> "${GITHUB_OUTPUT}" + echo "version=${version}" >> "${GITHUB_OUTPUT}" + echo "minor=${minor}" >> "${GITHUB_OUTPUT}" + echo "major=${major}" >> "${GITHUB_OUTPUT}" - name: Login to Docker Hub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - - name: Login to quay.io - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2 + - name: Login to Quay.io + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: + registry: quay.io username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_TOKEN }} - registry: quay.io - - name: Versioning - id: release - uses: cycjimmy/semantic-release-action@v3 - with: - semantic_version: 19.0.2 - extra_plugins: | - @semantic-release/changelog@6.0.1 - @semantic-release/git@10.0.1 - conventional-changelog-conventionalcommits@4.6.3 + - name: Build release image env: - GITHUB_TOKEN: ${{ secrets.GH_SECRET }} - - - uses: actions/setup-go@7b8cf10d4e4a01d4992d18a89f4d7dc5a3e6d6f4 # v4 - with: - go-version: 1.25 + VERSION: ${{ steps.release.outputs.version }} + run: make docker-build IMG="wandb/controller:${VERSION}" - - name: Install Ginkgo - run: go install github.com/onsi/ginkgo/v2/ginkgo@latest - - - name: Build and Push Latest - if: steps.release.outputs.new_release_version - run: | - export IMG=$IMAGE_TAG_BASE:$VERSION - make docker-build docker-push + - name: Publish Docker Hub tags env: - IMAGE_TAG_BASE: wandb/controller - VERSION: latest - - - name: Tag and Push to Docker Hub - if: steps.release.outputs.new_release_version + MAJOR: ${{ steps.release.outputs.major }} + MINOR: ${{ steps.release.outputs.minor }} + VERSION: ${{ steps.release.outputs.version }} run: | - docker tag wandb/controller:latest wandb/controller:${{ steps.release.outputs.new_release_version }} - docker push wandb/controller:${{ steps.release.outputs.new_release_version }} + set -euo pipefail - docker tag wandb/controller:latest wandb/controller:${{ steps.release.outputs.new_release_major_version }}.${{ steps.release.outputs.new_release_minor_version }} - docker push wandb/controller:${{ steps.release.outputs.new_release_major_version }}.${{ steps.release.outputs.new_release_minor_version }} + docker push "wandb/controller:${VERSION}" + docker tag "wandb/controller:${VERSION}" "wandb/controller:${MINOR}" + docker push "wandb/controller:${MINOR}" + docker tag "wandb/controller:${VERSION}" "wandb/controller:${MAJOR}" + docker push "wandb/controller:${MAJOR}" + docker tag "wandb/controller:${VERSION}" wandb/controller:latest + docker push wandb/controller:latest - docker tag wandb/controller:latest wandb/controller:${{ steps.release.outputs.new_release_major_version }} - docker push wandb/controller:${{ steps.release.outputs.new_release_major_version }} + - name: Publish Quay.io tags + env: + MAJOR: ${{ steps.release.outputs.major }} + MINOR: ${{ steps.release.outputs.minor }} + VERSION: ${{ steps.release.outputs.version }} + run: | + set -euo pipefail + quay_image="quay.io/wandb_tools/wandb-k8s-operator" + docker tag "wandb/controller:${VERSION}" "${quay_image}:${VERSION}" + docker push "${quay_image}:${VERSION}" + docker tag "wandb/controller:${VERSION}" "${quay_image}:${MINOR}" + docker push "${quay_image}:${MINOR}" + docker tag "wandb/controller:${VERSION}" "${quay_image}:${MAJOR}" + docker push "${quay_image}:${MAJOR}" - - name: Tag and Push to Quay.io - if: steps.release.outputs.new_release_version + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} + TAGGED_COMMIT: ${{ steps.release.outputs.tagged_commit }} run: | - docker tag wandb/controller:latest quay.io/wandb_tools/wandb-k8s-operator:${{ steps.release.outputs.new_release_version }} - docker push quay.io/wandb_tools/wandb-k8s-operator:${{ steps.release.outputs.new_release_version }} - - docker tag wandb/controller:latest quay.io/wandb_tools/wandb-k8s-operator:${{ steps.release.outputs.new_release_major_version }}.${{ steps.release.outputs.new_release_minor_version }} - docker push quay.io/wandb_tools/wandb-k8s-operator:${{ steps.release.outputs.new_release_major_version }}.${{ steps.release.outputs.new_release_minor_version }} - - docker tag wandb/controller:latest quay.io/wandb_tools/wandb-k8s-operator:${{ steps.release.outputs.new_release_major_version }} - docker push quay.io/wandb_tools/wandb-k8s-operator:${{ steps.release.outputs.new_release_major_version }} + gh release create "${TAG}" \ + --repo "${GITHUB_REPOSITORY}" \ + --target "${TAGGED_COMMIT}" \ + --verify-tag \ + --generate-notes \ + --title "${TAG}" diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index d10c97ee..6f0bb815 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -2,10 +2,10 @@ name: "Run Tests" on: push: branches: - - main + - v1 pull_request: branches: - - main + - v1 jobs: test: name: Test diff --git a/.releaserc.json b/.releaserc.json deleted file mode 100644 index abb5694d..00000000 --- a/.releaserc.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "branches": ["main"], - "ci": false, - "plugins": [ - ["@semantic-release/commit-analyzer", { "preset": "conventionalcommits" }], - [ - "@semantic-release/release-notes-generator", - { "preset": "conventionalcommits" } - ], - [ - "@semantic-release/github", - { - "successComment": "This ${issue.pull_request ? 'PR is included' : 'issue has been resolved'} in version ${nextRelease.version} :tada:", - "labels": false, - "releasedLabels": false - } - ], - [ - "@semantic-release/changelog", - { - "changelogFile": "CHANGELOG.md", - "changelogTitle": "# Changelog\n\nAll notable changes to this project will be documented in this file." - } - ], - [ - "@semantic-release/git", - { - "assets": ["CHANGELOG.md"], - "message": "chore(release): version ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" - } - ] - ] -} diff --git a/docs/releasing-v1.md b/docs/releasing-v1.md new file mode 100644 index 00000000..e16bb148 --- /dev/null +++ b/docs/releasing-v1.md @@ -0,0 +1,35 @@ +# Releasing Operator v1 + +Operator v1 releases are prepared through a reviewed pull request and published +from an annotated `v1.x.y` tag. The release workflow never writes to the `v1` +branch. + +1. Open a release pull request against `v1` containing the reviewed changelog + entry and all intended release changes. +2. Merge the pull request after its required checks and approvals pass. +3. Update the local branch and confirm it exactly matches the remote branch: + + ```bash + git switch v1 + git pull --ff-only origin v1 + test "$(git rev-parse HEAD)" = "$(git rev-parse origin/v1)" + ``` + +4. Create and push an annotated release tag at that commit: + + ```bash + version=v1.22.1 + git tag -a "${version}" -m "Operator ${version}" + git push origin "${version}" + ``` + +5. Monitor the `Release v1` GitHub Actions workflow. When it succeeds, record + the Docker Hub, Quay.io, and GitHub Release URLs and image digests in the + release record. +6. Never move, delete, or reuse an exact `v1.x.y` release tag. If a published + release is incorrect, fix it with a new patch version. + +The workflow publishes `1.x.y`, `1.x`, `1`, and `latest` to +`docker.io/wandb/controller`. It publishes `1.x.y`, `1.x`, and `1` to +`quay.io/wandb_tools/wandb-k8s-operator`; Quay.io does not receive a `latest` +tag. From 63bc72660a9c1328f73849196070dc1393970149 Mon Sep 17 00:00:00 2001 From: Casey Abernathy Date: Wed, 15 Jul 2026 10:26:34 -0700 Subject: [PATCH 16/49] feat: Promote Operator v2 to main (#261) Co-authored-by: Daniel Panzella Co-authored-by: jonathan meeks Co-authored-by: Kevin Chen <140976422+wandb-kc@users.noreply.github.com> Co-authored-by: Jonathan Meeks Co-authored-by: Jonathan Meeks Co-authored-by: Claude Sonnet 4.5 (1M context) Co-authored-by: amwarrier Co-authored-by: Zachary Blasczyk <77289967+zacharyblasczyk@users.noreply.github.com> Co-authored-by: semantic-release-bot Co-authored-by: wnevis-cmyk Co-authored-by: Jagat Thakkar <32109558+jthakkar04@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Collin Olander --- .../skills/convert-datadog-dashboard/SKILL.md | 173 + .../references/query-mapping.md | 172 + .../references/scrub-checklist.md | 74 + .../references/widget-mapping.md | 87 + .github/workflows/chart-validation.yaml | 71 + .github/workflows/docker-build-scan.yml | 16 +- .github/workflows/internal-chart-publish.yaml | 84 +- .github/workflows/internal-image-publish.yaml | 42 +- .github/workflows/release.yaml | 129 +- .github/workflows/run-tests.yaml | 32 +- .gitignore | 15 +- .golangci.yml | 22 + CLAUDE.md | 122 + DEVELOPMENT.md | 217 + Dockerfile | 15 +- Makefile | 102 +- PROJECT | 28 + README.md | 225 +- Tiltfile | 972 +- Tiltfile.chart | 42 + api/v1/weightsandbiases_conversion.go | 160 + api/v1/weightsandbiases_conversion_mapping.go | 740 + .../weightsandbiases_conversion_overrides.go | 367 + ...ghtsandbiases_conversion_overrides_test.go | 495 + api/v1/weightsandbiases_conversion_test.go | 1803 ++ api/v2/application_types.go | 137 + api/v2/groupversion_info.go | 36 + api/v2/weightsandbiases_conversion.go | 22 + api/v2/weightsandbiases_types.go | 785 + api/v2/zz_generated.deepcopy.go | 1703 ++ cmd/crd-installer/main.go | 207 + cmd/{ => manager}/main.go | 220 +- config/certmanager/certificate-metrics.yaml | 20 + config/certmanager/certificate-webhook.yaml | 20 + config/certmanager/issuer.yaml | 13 + config/certmanager/kustomization.yaml | 7 + config/certmanager/kustomizeconfig.yaml | 8 + .../bases/apps.wandb.com_applications.yaml | 13929 ++++++++++++++++ .../apps.wandb.com_weightsandbiases.yaml | 6213 ++++++- config/crd/bases/kustomization.yaml | 3 + config/crd/kustomization.yaml | 16 +- .../patches/webhook_in_weightsandbiases.yaml | 16 + config/default/kustomization.yaml | 250 +- config/default/manager_webhook_patch.yaml | 31 + .../dev-common/delete-applications-crd.yaml | 5 + .../delete-weightsandbiases-crd.yaml | 5 + config/dev-common/kustomization.yaml | 16 + config/local-dev/README.md | 227 + config/local-dev/kustomization.yaml | 12 + config/local-dev/webhook_local_patch.yaml | 22 + config/manager/manager.yaml | 10 +- .../network-policy/allow-webhook-traffic.yaml | 27 + config/network-policy/kustomization.yaml | 1 + config/openshift-dev/README.md | 104 + config/openshift-dev/kustomization.yaml | 13 + config/openshift-dev/openshift-security.yaml | 19 + config/rbac/application_admin_role.yaml | 27 + config/rbac/application_editor_role.yaml | 33 + config/rbac/application_viewer_role.yaml | 29 + config/rbac/kustomization.yaml | 3 + config/rbac/role.yaml | 206 +- config/samples/apps_v2_application.yaml | 45 + config/samples/apps_v2_weightsandbiases.yaml | 24 + config/samples/kafka-operator-configmap.yaml | 12 + config/samples/kustomization.yaml | 2 + config/tilt-dev/kustomization.yaml | 13 + config/tilt-dev/security.yaml | 9 + config/webhook/kustomization.yaml | 9 + config/webhook/kustomizeconfig.yaml | 22 + config/webhook/manifests.yaml | 92 + config/webhook/service.yaml | 16 + deploy/.yamllint.yaml | 11 + deploy/ct.yaml | 15 + deploy/operator/Chart.lock | 30 + deploy/operator/Chart.yaml | 48 + deploy/operator/profiles/openshift.yaml | 44 + .../operator/profiles/telemetry-forward.yaml | 8 + deploy/operator/profiles/telemetry-full.yaml | 10 + deploy/operator/profiles/telemetry-off.yaml | 8 + deploy/operator/templates/_helpers.tpl | 39 + deploy/operator/templates/certificates.yaml | 24 + .../templates/hooks/crd-installer-job.yaml | 89 + deploy/operator/templates/hooks/rbac.yaml | 48 + .../openshift-owner-finalizers-rbac.yaml | 56 + .../templates/openshift-scc-rbac.yaml | 35 + ...eaweedfs-operator-servicemonitor-role.yaml | 32 + .../operator/templates/telemetry-config.yaml | 20 + .../templates/telemetry-validation.yaml | 26 + deploy/operator/templates/wandb-cr.yaml | 44 + .../wandb-operator-application-role.yaml | 71 + .../wandb-operator-clickhouse-role.yaml | 57 + .../wandb-operator-grafana-role.yaml | 45 + .../templates/wandb-operator-mysql-role.yaml | 41 + .../templates/wandb-operator-redis-role.yaml | 41 + .../wandb-operator-seaweedfs-role.yaml | 40 + .../templates/wandb-operator-vm-role.yaml | 53 + .../templates/wandb-operator-wandb-role.yaml | 213 + deploy/operator/templates/webhooks.yaml | 98 + deploy/operator/values.schema.json | 152 + deploy/operator/values.yaml | 258 + deploy/telemetry/Chart.yaml | 10 + .../dashboards/wandb-application.json | 558 + .../dashboards/wandb-field-investigation.json | 324 + .../wandb-managed-install-performance.json | 579 + .../dashboards/wandb-telemetry-overview.json | 2266 +++ deploy/telemetry/templates/_helpers.tpl | 59 + .../templates/telemetry-alerting.yaml | 58 + .../templates/telemetry-otlp-gateway.yaml | 220 + .../templates/telemetry-scrapes.yaml | 403 + deploy/telemetry/templates/telemetry-ui.yaml | 199 + .../templates/telemetry-victoria-core.yaml | 50 + deploy/telemetry/templates/validation.yaml | 4 + deploy/telemetry/values.schema.json | 124 + deploy/telemetry/values.yaml | 37 + docs/design/wandb_v2/infra_reconciliation.md | 108 + docs/design/wandb_v2/legacy_overrides.md | 379 + docs/design/wandb_v2/struct_usage_graph.md | 214 + docs/design/wandb_v2/struct_usage_mapping.md | 1143 ++ docs/design/wandb_v2/tilt.md | 90 + docs/infra-connection-settings.md | 110 + docs/monitoring.md | 156 + docs/openshift.md | 201 + docs/plan-ingress-gateway-api.md | 990 ++ docs/releasing.md | 61 + ginko.mk | 53 - go.mod | 182 +- go.sum | 406 +- golangci.yaml | 3 - hack/scripts/kind-images-manager.sh | 100 + hack/scripts/list-crs.sh | 81 + .../managed-connections-to-external.sh | 65 + hack/scripts/pull-server-manifest.sh | 94 + hack/scripts/setup_crc.sh | 89 + hack/scripts/setup_kind.sh | 52 + hack/scripts/teardown_kind.sh | 35 + hack/scripts/test-weave.py | 64 + hack/scripts/tilt-dev-clean.sh | 182 + hack/scripts/tilt-down-dev-clean.sh | 27 + hack/scripts/verify-custom-ca-e2e.sh | 303 + .../cert-manager/ca-certificate.yaml | 11 + .../cert-manager/ca-issuer.yaml | 7 + .../cert-manager/selfsigned-issuer.yaml | 6 + hack/testing-manifests/minio/minio.yaml | 51 - .../seaweedfs/seaweedfs.yaml | 49 + .../0.83.0-clickhouse-keeper.1/manifest.yaml | 960 ++ .../0.83.0-clickhouse-keeper.1/sizing.yaml | 1517 ++ .../0.83.0-clickhouse-keeper.2/manifest.yaml | 960 ++ .../0.83.0-clickhouse-keeper.2/sizing.yaml | 1518 ++ .../telemetry/grafana-dev.yaml | 76 + .../telemetry/infra-metrics-dev.yaml | 139 + .../telemetry/kube-metrics-dev.yaml | 21 + .../telemetry/operator-metrics-dev.yaml | 61 + .../telemetry/victoria-dev.yaml | 36 + .../telemetry/wandb-otel-connection-dev.yaml | 14 + hack/testing-manifests/test-infra/Chart.yaml | 6 + .../examples/wandb-external-v1.yaml | 95 + .../examples/wandb-external-v2.yaml | 77 + .../test-infra/templates/_helpers.tpl | 48 + .../test-infra/templates/certificates.yaml | 100 + .../templates/mysql-deployment.yaml | 81 + .../test-infra/templates/mysql-secret.yaml | 15 + .../test-infra/templates/mysql-service.yaml | 17 + .../templates/redis-deployment.yaml | 85 + .../test-infra/templates/redis-secret.yaml | 13 + .../test-infra/templates/redis-service.yaml | 17 + .../seaweedfs-create-bucket-job.yaml | 58 + .../templates/seaweedfs-deployment.yaml | 81 + .../templates/seaweedfs-secret.yaml | 17 + .../templates/seaweedfs-service.yaml | 17 + hack/testing-manifests/test-infra/values.yaml | 64 + .../wandb/.generated/.gitkeep | 0 .../wandb/custom-ca-e2e/README.md | 52 + .../wandb/kustomize/base/kustomization.yaml | 4 + .../wandb/kustomize/base/wandb.yaml | 36 + .../disable-clickhouse/kustomization.yaml | 4 + .../overlays/disable-clickhouse/patch.yaml | 8 + .../overlays/disable-kafka/kustomization.yaml | 4 + .../overlays/disable-kafka/patch.yaml | 7 + .../overlays/disable-mysql/kustomization.yaml | 4 + .../overlays/disable-mysql/patch.yaml | 8 + .../disable-objectstore/kustomization.yaml | 4 + .../overlays/disable-objectstore/patch.yaml | 8 + .../overlays/disable-redis/kustomization.yaml | 4 + .../overlays/disable-redis/patch.yaml | 8 + .../external-clickhouse/kustomization.yaml | 4 + .../overlays/external-clickhouse/patch.yaml | 27 + .../external-mysql/kustomization.yaml | 4 + .../overlays/external-mysql/patch.yaml | 40 + .../external-objectstore/kustomization.yaml | 4 + .../overlays/external-objectstore/patch.yaml | 34 + .../external-redis/kustomization.yaml | 4 + .../overlays/external-redis/patch.yaml | 28 + .../overlays/license-file/.gitignore | 1 + .../overlays/license-file/kustomization.yaml | 4 + .../kustomization.yaml | 4 + .../networking-gateway-local/patch.yaml | 19 + .../kustomization.yaml | 4 + .../networking-ingress-local/patch.yaml | 11 + .../purge-retention/kustomization.yaml | 4 + .../overlays/purge-retention/patch.yaml | 7 + .../server-version-0.78.0/kustomization.yaml | 4 + .../overlays/server-version-0.78.0/patch.yaml | 7 + .../overlays/size-micro/kustomization.yaml | 4 + .../kustomize/overlays/size-micro/patch.yaml | 6 + .../overlays/size-small/kustomization.yaml | 4 + .../kustomize/overlays/size-small/patch.yaml | 6 + .../wandb/local-registry.yaml | 6 +- .../{default.yaml => wandb-default-v1.yaml} | 54 +- .../testing-manifests/wandb/wandb-dev-v2.yaml | 36 + .../wandb/wandb-legacy-overrides-v1.yaml | 71 + hack/tilt/wandbcr/main.go | 614 + hack/tilt/wandbcr/main_test.go | 499 + internal/controller/application_controller.go | 1273 ++ .../controller/application_controller_test.go | 743 + .../controller/application_http_route_test.go | 70 + .../controller/application_service_test.go | 94 + internal/controller/common/co_managed.go | 74 + .../controller/common/common_suite_test.go | 13 + internal/controller/common/condition.go | 139 + internal/controller/common/condition_test.go | 940 ++ internal/controller/common/detach.go | 24 + internal/controller/common/labels.go | 32 + internal/controller/common/naming.go | 54 + internal/controller/common/naming_test.go | 49 + internal/controller/common/pods.go | 17 + internal/controller/common/resource.go | 222 + internal/controller/common/resource_test.go | 123 + internal/controller/common/retention.go | 35 + internal/controller/common/service.go | 19 + internal/controller/common/service_test.go | 25 + internal/controller/common/state.go | 14 + internal/controller/ctrlqueue/ctrl_state.go | 63 + .../controller/ctrlqueue/ctrl_state_test.go | 271 + .../infra/external/clickhouse/clickhouse.go | 89 + internal/controller/infra/external/common.go | 173 + .../controller/infra/external/mysql/mysql.go | 127 + .../infra/external/mysql/mysql_test.go | 90 + .../infra/external/objectstore/connection.go | 135 + .../external/objectstore/connection_test.go | 108 + .../infra/external/objectstore/objectstore.go | 198 + .../external/objectstore/objectstore_test.go | 342 + .../infra/external/objectstore/pathstyle.go | 31 + .../external/objectstore/pathstyle_test.go | 40 + .../controller/infra/external/redis/redis.go | 113 + .../infra/external/redis/redis_test.go | 79 + .../altinity/altinity_suite_test.go | 13 + .../infra/managed/clickhouse/altinity/conn.go | 117 + .../managed/clickhouse/altinity/detach.go | 80 + .../managed/clickhouse/altinity/image_test.go | 31 + .../altinity/keeper/keeper_suite_test.go | 13 + .../clickhouse/altinity/keeper/naming.go | 35 + .../clickhouse/altinity/keeper/read.go | 104 + .../clickhouse/altinity/keeper/read_test.go | 29 + .../clickhouse/altinity/keeper/spec.go | 157 + .../clickhouse/altinity/keeper/spec_test.go | 119 + .../clickhouse/altinity/keeper/values.go | 29 + .../clickhouse/altinity/keeper/write.go | 71 + .../managed/clickhouse/altinity/naming.go | 153 + .../clickhouse/altinity/naming_test.go | 87 + .../clickhouse/altinity/objectstorage.go | 208 + .../clickhouse/altinity/objectstorage_test.go | 109 + .../managed/clickhouse/altinity/purge.go | 66 + .../infra/managed/clickhouse/altinity/read.go | 216 + .../infra/managed/clickhouse/altinity/spec.go | 334 + .../managed/clickhouse/altinity/spec_test.go | 242 + .../managed/clickhouse/altinity/status.go | 245 + .../clickhouse/altinity/status_test.go | 58 + .../managed/clickhouse/altinity/values.go | 34 + .../managed/clickhouse/altinity/write.go | 107 + .../infra/managed/kafka/bufstream/config.go | 181 + .../managed/kafka/bufstream/config_test.go | 112 + .../infra/managed/kafka/bufstream/conn.go | 99 + .../infra/managed/kafka/bufstream/detach.go | 111 + .../managed/kafka/bufstream/detach_test.go | 69 + .../managed/kafka/bufstream/image_test.go | 53 + .../infra/managed/kafka/bufstream/naming.go | 151 + .../infra/managed/kafka/bufstream/purge.go | 58 + .../infra/managed/kafka/bufstream/read.go | 89 + .../infra/managed/kafka/bufstream/spec.go | 604 + .../managed/kafka/bufstream/spec_test.go | 330 + .../infra/managed/kafka/bufstream/status.go | 155 + .../infra/managed/kafka/bufstream/values.go | 82 + .../infra/managed/kafka/bufstream/write.go | 236 + .../managed/kafka/bufstream/write_test.go | 102 + .../infra/managed/mysql/moco/conn.go | 103 + .../infra/managed/mysql/moco/detach.go | 99 + .../infra/managed/mysql/moco/image_test.go | 31 + .../managed/mysql/moco/mysql_suite_test.go | 13 + .../infra/managed/mysql/moco/naming.go | 67 + .../infra/managed/mysql/moco/purge.go | 85 + .../infra/managed/mysql/moco/read.go | 150 + .../infra/managed/mysql/moco/spec.go | 197 + .../infra/managed/mysql/moco/spec_test.go | 291 + .../infra/managed/mysql/moco/status.go | 215 + .../infra/managed/mysql/moco/write.go | 225 + .../managed/objectstore/seaweedfs/config.go | 69 + .../objectstore/seaweedfs/config_test.go | 64 + .../managed/objectstore/seaweedfs/conn.go | 156 + .../managed/objectstore/seaweedfs/detach.go | 94 + .../objectstore/seaweedfs/image_test.go | 31 + .../objectstore/seaweedfs/legacy_minio.go | 173 + .../managed/objectstore/seaweedfs/naming.go | 89 + .../managed/objectstore/seaweedfs/purge.go | 83 + .../managed/objectstore/seaweedfs/read.go | 270 + .../objectstore/seaweedfs/read_test.go | 81 + .../seaweedfs/seaweedfs_suite_test.go | 13 + .../managed/objectstore/seaweedfs/spec.go | 258 + .../objectstore/seaweedfs/spec_test.go | 352 + .../managed/objectstore/seaweedfs/status.go | 245 + .../objectstore/seaweedfs/status_test.go | 69 + .../managed/objectstore/seaweedfs/write.go | 223 + .../objectstore/seaweedfs/write_test.go | 87 + .../infra/managed/redis/opstree/conn.go | 102 + .../infra/managed/redis/opstree/detach.go | 90 + .../infra/managed/redis/opstree/image_test.go | 76 + .../infra/managed/redis/opstree/naming.go | 92 + .../redis/opstree/opstree_suite_test.go | 13 + .../infra/managed/redis/opstree/purge.go | 63 + .../infra/managed/redis/opstree/read.go | 405 + .../infra/managed/redis/opstree/spec.go | 408 + .../infra/managed/redis/opstree/spec_test.go | 170 + .../infra/managed/redis/opstree/status.go | 260 + .../infra/managed/redis/opstree/values.go | 13 + .../infra/managed/redis/opstree/write.go | 386 + .../reconciler/cleanup_legacy_v1.go | 113 + .../reconciler/cleanup_legacy_v1_test.go | 287 + internal/controller/reconciler/clickhouse.go | 310 + internal/controller/reconciler/custom_ca.go | 477 + .../controller/reconciler/custom_ca_test.go | 270 + internal/controller/reconciler/gateway.go | 360 + .../controller/reconciler/infra_routes.go | 462 + internal/controller/reconciler/ingress.go | 236 + .../controller/reconciler/ingress_test.go | 51 + internal/controller/reconciler/kafka.go | 267 + .../controller/reconciler/legacy_overrides.go | 77 + .../reconciler/legacy_overrides_test.go | 181 + .../controller/reconciler/manifest_order.go | 25 + .../controller/reconciler/migrate_legacy.go | 434 + .../reconciler/migrate_legacy_test.go | 719 + internal/controller/reconciler/mysql.go | 460 + .../reconciler/networking_cleanup.go | 57 + .../networking_route_builders_test.go | 146 + internal/controller/reconciler/objectstore.go | 285 + .../controller/reconciler/oidc_env_test.go | 126 + internal/controller/reconciler/pods.go | 520 + .../reconciler/pods_instance_test.go | 80 + internal/controller/reconciler/pods_test.go | 48 + .../controller/reconciler/probe_defaults.go | 103 + .../reconciler/probe_defaults_test.go | 371 + internal/controller/reconciler/rbac.go | 254 + .../controller/reconciler/reconcile_v2.go | 1544 ++ .../reconcile_v2_infra_sizing_test.go | 258 + .../reconcile_v2_objectstore_sizing_test.go | 112 + .../reconciler/reconcile_v2_sizing_test.go | 297 + internal/controller/reconciler/redis.go | 264 + internal/controller/reconciler/sizing.go | 368 + .../controller/reconciler/status_update.go | 21 + .../reconciler/status_update_test.go | 71 + .../reconciler/telemetry_chart_test.go | 170 + .../controller/reconciler/telemetry_config.go | 124 + .../reconciler/telemetry_configmap.go | 74 + .../controller/reconciler/telemetry_secret.go | 124 + .../controller/reconciler/telemetry_status.go | 255 + .../controller/reconciler/telemetry_test.go | 1345 ++ .../controller/reconciler/v2_suite_test.go | 30 + internal/controller/suite_test.go | 103 +- .../controller/weightsandbiases_controller.go | 473 +- ...htsandbiases_controller_networking_test.go | 363 + .../weightsandbiases_controller_test.go | 964 +- internal/crdinstaller/apply.go | 94 + internal/crdinstaller/compose.go | 232 + internal/crdinstaller/compose_test.go | 146 + ....altinity.com_clickhouseinstallations.yaml | 9468 +++++++++++ .../operator/apps.wandb.com_applications.yaml | 13929 ++++++++++++++++ .../apps.wandb.com_weightsandbiases.yaml | 6244 +++++++ .../redis.redis.opstreelabs.in_redis.yaml | 5392 ++++++ ...is.redis.opstreelabs.in_redisclusters.yaml | 7862 +++++++++ ...edis.opstreelabs.in_redisreplications.yaml | 5593 +++++++ ...s.redis.opstreelabs.in_redissentinels.yaml | 5201 ++++++ internal/crdinstaller/embed.go | 42 + internal/crdinstaller/render.go | 59 + internal/logx/attrs.go | 7 + internal/logx/context.go | 25 + internal/logx/handler.go | 76 + internal/logx/options.go | 68 + internal/logx/pretty.go | 21 + internal/logx/settings.go | 30 + internal/metrics/metrics.go | 48 + internal/metrics/metrics_test.go | 86 + internal/probes/probes.go | 131 + internal/webhook/v2/application_webhook.go | 145 + .../webhook/v2/application_webhook_test.go | 111 + internal/webhook/v2/webhook_suite_test.go | 167 + ...ghtsandbiases_defaulter_clickhouse_test.go | 149 + .../weightsandbiases_defaulter_kafka_test.go | 73 + .../weightsandbiases_defaulter_mysql_test.go | 80 + ...htsandbiases_defaulter_objectstore_test.go | 84 + .../weightsandbiases_defaulter_redis_test.go | 75 + .../weightsandbiases_multi_instance_test.go | 85 + .../v2/weightsandbiases_probe_defaults.go | 52 + .../weightsandbiases_probe_defaults_test.go | 202 + .../webhook/v2/weightsandbiases_webhook.go | 855 + .../v2/weightsandbiases_webhook_test.go | 340 + pkg/helm/chart.go | 106 +- pkg/utils/coalesce.go | 29 + pkg/utils/coalesce_test.go | 242 + pkg/utils/connection_secrets.go | 38 + pkg/utils/gvk.go | 28 + pkg/utils/gvk_test.go | 32 + pkg/utils/maps.go | 15 + pkg/utils/maps_test.go | 161 + pkg/utils/merge.go | 19 + pkg/utils/openshift.go | 25 + pkg/utils/slices.go | 40 + pkg/utils/strings.go | 53 + pkg/utils/utils_suite_test.go | 13 + pkg/vendored/altinity-clickhouse/README.md | 154 + .../api_group.go | 20 + .../v1/api_register.go | 45 + .../v1/api_resources.go | 24 + .../v1/api_version.go | 20 + .../clickhouse-keeper.altinity.com/v1/doc.go | 19 + .../v1/type_chk.go | 715 + .../v1/type_cluster.go | 434 + .../v1/type_configuration.go | 82 + .../v1/type_replica.go | 202 + .../v1/type_shard.go | 310 + .../v1/type_spec.go | 130 + .../v1/type_status.go | 899 + .../v1/types.go | 74 + .../v1/zz_generated.deepcopy.go | 682 + .../clickhouse.altinity.com/api_group.go | 20 + .../clickhouse.altinity.com/v1/action_plan.go | 425 + .../v1/api_register.go | 54 + .../v1/api_resources.go | 26 + .../clickhouse.altinity.com/v1/api_version.go | 20 + .../clickhouse.altinity.com/v1/cr.go | 168 + .../clickhouse.altinity.com/v1/doc.go | 19 + .../clickhouse.altinity.com/v1/interface.go | 309 + .../v1/type_attributes.go | 144 + .../clickhouse.altinity.com/v1/type_chi.go | 740 + .../v1/type_chi_templating.go | 82 + .../v1/type_cleanup.go | 252 + .../v1/type_cluster.go | 476 + .../v1/type_cluster_secret.go | 99 + .../v1/type_configuration_chi.go | 123 + .../v1/type_configuration_chop.go | 1607 ++ .../v1/type_defaults.go | 66 + .../v1/type_distributed_ddl.go | 66 + .../clickhouse.altinity.com/v1/type_host.go | 667 + .../v1/type_host_address.go | 223 + .../v1/type_hosts_field.go | 80 + .../v1/type_reconcile.go | 277 + .../v1/type_reconcile_macros.go | 57 + .../v1/type_reconcile_runtime.go | 42 + .../v1/type_replica.go | 200 + .../v1/type_setting.go | 425 + .../v1/type_setting_data_source.go | 145 + .../v1/type_setting_scalar.go | 154 + .../v1/type_setting_vector.go | 92 + .../v1/type_settings.go | 857 + .../type_settings_name2key_converter_plain.go | 38 + ...type_settings_name2key_converter_prefix.go | 56 + .../v1/type_settings_section.go | 63 + .../v1/type_settings_user.go | 39 + .../clickhouse.altinity.com/v1/type_shard.go | 309 + .../clickhouse.altinity.com/v1/type_spec.go | 171 + .../clickhouse.altinity.com/v1/type_status.go | 902 + .../v1/type_status_test.go | 253 + .../v1/type_storage_management.go | 68 + .../v1/type_target_selector.go | 48 + .../v1/type_template_indexes.go | 231 + .../v1/type_template_ref.go | 22 + .../v1/type_templates.go | 433 + .../v1/type_templates_list.go | 305 + .../v1/type_volume_claim_template.go | 112 + .../v1/type_zookeeper.go | 130 + .../v1/type_zookeeper_node.go | 72 + .../clickhouse.altinity.com/v1/types.go | 120 + .../v1/zz_generated.deepcopy.go | 3036 ++++ .../common/types/copy_cr_options.go | 23 + .../common/types/data_source.go | 44 + .../common/types/host_scope_address.go | 132 + .../altinity-clickhouse/common/types/id.go | 60 + .../altinity-clickhouse/common/types/int32.go | 118 + .../altinity-clickhouse/common/types/list.go | 21 + .../common/types/matchable.go | 35 + .../common/types/object_address.go | 33 + .../common/types/object_status.go | 37 + .../altinity-clickhouse/common/types/port.go | 64 + .../common/types/reconcile_attributes.go | 219 + .../common/types/status_options.go | 76 + .../common/types/string.go | 91 + .../common/types/string_bool.go | 230 + .../common/types/strings.go | 124 + .../altinity-clickhouse/common/types/tags.go | 100 + .../clickhouse-operator-install-bundle.yaml | 6256 +++++++ ....altinity.com_clickhouseinstallations.yaml | 9468 +++++++++++ .../deployment/affinity.go | 62 + .../deployment/env_vars.go | 56 + .../metrics/watched_resource.go | 156 + .../altinity-clickhouse/swversion/min_max.go | 23 + .../swversion/software_version.go | 190 + .../altinity-clickhouse/util/array.go | 204 + .../altinity-clickhouse/util/context.go | 85 + pkg/vendored/altinity-clickhouse/util/dir.go | 58 + pkg/vendored/altinity-clickhouse/util/dump.go | 25 + .../altinity-clickhouse/util/duration.go | 30 + .../altinity-clickhouse/util/fingerprint.go | 20 + pkg/vendored/altinity-clickhouse/util/fqdn.go | 32 + pkg/vendored/altinity-clickhouse/util/fs.go | 75 + pkg/vendored/altinity-clickhouse/util/hash.go | 78 + pkg/vendored/altinity-clickhouse/util/int.go | 31 + pkg/vendored/altinity-clickhouse/util/k8s.go | 83 + pkg/vendored/altinity-clickhouse/util/map.go | 350 + .../altinity-clickhouse/util/messagediff.go | 98 + .../altinity-clickhouse/util/print.go | 37 + .../altinity-clickhouse/util/prometheus.go | 76 + .../altinity-clickhouse/util/replacer.go | 221 + .../altinity-clickhouse/util/retry/retry.go | 65 + .../altinity-clickhouse/util/rfc1035.go | 65 + .../util/runtime/runtime.go | 54 + .../altinity-clickhouse/util/shell.go | 87 + .../altinity-clickhouse/util/slice.go | 24 + .../altinity-clickhouse/util/string.go | 93 + .../altinity-clickhouse/version/version.go | 24 + pkg/vendored/altinity-clickhouse/xml/xml.go | 258 + pkg/vendored/argo-rollouts/README.md | 26 + .../argoproj.io.rollouts/register.go | 46 + .../v1alpha1/analysis_types.go | 640 + .../argoproj.io.rollouts/v1alpha1/doc.go | 6 + .../v1alpha1/experiment_types.go | 247 + .../argoproj.io.rollouts/v1alpha1/register.go | 54 + .../argoproj.io.rollouts/v1alpha1/types.go | 1193 ++ .../v1alpha1/types_test.go | 26 + .../v1alpha1/zz_generated.deepcopy.go | 3179 ++++ .../argo-rollouts/crds/analysis-run-crd.yaml | 3319 ++++ .../crds/analysis-template-crd.yaml | 3190 ++++ .../crds/cluster-analysis-template-crd.yaml | 3190 ++++ .../argo-rollouts/crds/experiment-crd.yaml | 2858 ++++ .../argo-rollouts/crds/rollout-crd.yaml | 3908 +++++ ....networking.k8s.io_backendtlspolicies.yaml | 1382 ++ ...eway.networking.k8s.io_gatewayclasses.yaml | 515 + .../gateway.networking.k8s.io_gateways.yaml | 3283 ++++ .../gateway.networking.k8s.io_grpcroutes.yaml | 2070 +++ .../gateway.networking.k8s.io_httproutes.yaml | 6921 ++++++++ ...ateway.networking.k8s.io_listenersets.yaml | 780 + ...way.networking.k8s.io_referencegrants.yaml | 353 + .../gateway.networking.k8s.io_tlsroutes.yaml | 2114 +++ ...ay.networking.k8s.io_vap_safeupgrades.yaml | 48 + pkg/vendored/moco/crds/moco_crds.yaml | 8620 ++++++++++ pkg/vendored/redis-operator/README.md | 109 + .../redis-operator/common/v1beta2/README.md | 19 + .../common/v1beta2/common_types.go | 245 + .../common/v1beta2/common_types_test.go | 86 + .../redis-operator/common/v1beta2/doc.go | 19 + .../common/v1beta2/zz_generated.deepcopy.go | 630 + .../redis.redis.opstreelabs.in_redis.yaml | 5392 ++++++ ...is.redis.opstreelabs.in_redisclusters.yaml | 7862 +++++++++ ...edis.opstreelabs.in_redisreplications.yaml | 5593 +++++++ ...s.redis.opstreelabs.in_redissentinels.yaml | 5201 ++++++ .../redis-operator/redis/v1beta2/README.md | 30 + .../redis/v1beta2/groupversion_info.go | 34 + .../redis/v1beta2/redis_conversion.go | 4 + .../redis/v1beta2/redis_types.go | 74 + .../redis/v1beta2/redis_webhook.go | 29 + .../redis/v1beta2/zz_generated.deepcopy.go | 227 + .../redisreplication/v1beta2/README.md | 22 + .../v1beta2/groupversion_info.go | 34 + .../v1beta2/redisreplication_conversion.go | 4 + .../v1beta2/redisreplication_types.go | 68 + .../v1beta2/redisreplication_webhook.go | 29 + .../v1beta2/zz_generated.deepcopy.go | 244 + .../redissentinel/v1beta2/README.md | 41 + .../v1beta2/groupversion_info.go | 34 + .../v1beta2/redissentinel_conversion.go | 4 + .../v1beta2/redissentinel_types.go | 68 + .../v1beta2/redissentinel_webhook.go | 102 + .../v1beta2/zz_generated.deepcopy.go | 255 + pkg/vendored/seaweedfs-operator/README.md | 48 + .../crds/seaweed.seaweedfs.com_seaweeds.yaml | 10727 ++++++++++++ .../v1/groupversion_info.go | 31 + .../seaweed.seaweedfs.com/v1/types.go | 305 + .../v1/vendored_helpers.go | 5 + .../v1/zz_generated.deepcopy.go | 934 ++ pkg/wandb/manifest/image_ref_test.go | 88 + .../manifest/load_manifest_from_files_test.go | 97 + pkg/wandb/manifest/manifest.go | 846 + pkg/wandb/manifest/manifest_decode_test.go | 53 + pkg/wandb/manifest/manifest_suite_test.go | 13 + .../spec/channel/deployer/deployer_test.go | 3 +- pkg/wandb/spec/charts/local.go | 4 +- pkg/wandb/spec/charts/oci.go | 8 +- pkg/wandb/spec/charts/oci_test.go | 2 +- pkg/wandb/spec/charts/repo.go | 24 +- pkg/wandb/spec/charts/repo_test.go | 29 +- pkg/wandb/spec/charts/repo_test_helpers.go | 37 - pkg/wandb/spec/config.go | 6 +- pkg/wandb/spec/spec.go | 2 +- pkg/wandb/spec/specfakes/fake_chart.go | 22 +- pkg/wandb/spec/state/versioning.go | 1 + pkg/wandb/spec/state/versioning_test.go | 2 +- test/e2e/e2e_suite_test.go | 11 + test/e2e/e2e_test.go | 52 + test/e2e/retention_test.go | 567 + test/utils/utils.go | 4 +- tilt-settings.sample.json | 11 - tilt-settings.sample.star | 56 + 608 files changed, 263793 insertions(+), 1474 deletions(-) create mode 100644 .claude/skills/convert-datadog-dashboard/SKILL.md create mode 100644 .claude/skills/convert-datadog-dashboard/references/query-mapping.md create mode 100644 .claude/skills/convert-datadog-dashboard/references/scrub-checklist.md create mode 100644 .claude/skills/convert-datadog-dashboard/references/widget-mapping.md create mode 100644 .github/workflows/chart-validation.yaml create mode 100644 .golangci.yml create mode 100644 CLAUDE.md create mode 100644 DEVELOPMENT.md create mode 100644 Tiltfile.chart create mode 100644 api/v1/weightsandbiases_conversion.go create mode 100644 api/v1/weightsandbiases_conversion_mapping.go create mode 100644 api/v1/weightsandbiases_conversion_overrides.go create mode 100644 api/v1/weightsandbiases_conversion_overrides_test.go create mode 100644 api/v1/weightsandbiases_conversion_test.go create mode 100644 api/v2/application_types.go create mode 100644 api/v2/groupversion_info.go create mode 100644 api/v2/weightsandbiases_conversion.go create mode 100644 api/v2/weightsandbiases_types.go create mode 100644 api/v2/zz_generated.deepcopy.go create mode 100644 cmd/crd-installer/main.go rename cmd/{ => manager}/main.go (59%) create mode 100644 config/certmanager/certificate-metrics.yaml create mode 100644 config/certmanager/certificate-webhook.yaml create mode 100644 config/certmanager/issuer.yaml create mode 100644 config/certmanager/kustomization.yaml create mode 100644 config/certmanager/kustomizeconfig.yaml create mode 100644 config/crd/bases/apps.wandb.com_applications.yaml create mode 100644 config/crd/bases/kustomization.yaml create mode 100644 config/crd/patches/webhook_in_weightsandbiases.yaml create mode 100644 config/default/manager_webhook_patch.yaml create mode 100644 config/dev-common/delete-applications-crd.yaml create mode 100644 config/dev-common/delete-weightsandbiases-crd.yaml create mode 100644 config/dev-common/kustomization.yaml create mode 100644 config/local-dev/README.md create mode 100644 config/local-dev/kustomization.yaml create mode 100644 config/local-dev/webhook_local_patch.yaml create mode 100644 config/network-policy/allow-webhook-traffic.yaml create mode 100644 config/openshift-dev/README.md create mode 100644 config/openshift-dev/kustomization.yaml create mode 100644 config/openshift-dev/openshift-security.yaml create mode 100644 config/rbac/application_admin_role.yaml create mode 100644 config/rbac/application_editor_role.yaml create mode 100644 config/rbac/application_viewer_role.yaml create mode 100644 config/samples/apps_v2_application.yaml create mode 100644 config/samples/apps_v2_weightsandbiases.yaml create mode 100644 config/samples/kafka-operator-configmap.yaml create mode 100644 config/tilt-dev/kustomization.yaml create mode 100644 config/tilt-dev/security.yaml create mode 100644 config/webhook/kustomization.yaml create mode 100644 config/webhook/kustomizeconfig.yaml create mode 100644 config/webhook/manifests.yaml create mode 100644 config/webhook/service.yaml create mode 100644 deploy/.yamllint.yaml create mode 100644 deploy/ct.yaml create mode 100644 deploy/operator/Chart.lock create mode 100644 deploy/operator/Chart.yaml create mode 100644 deploy/operator/profiles/openshift.yaml create mode 100644 deploy/operator/profiles/telemetry-forward.yaml create mode 100644 deploy/operator/profiles/telemetry-full.yaml create mode 100644 deploy/operator/profiles/telemetry-off.yaml create mode 100644 deploy/operator/templates/_helpers.tpl create mode 100644 deploy/operator/templates/certificates.yaml create mode 100644 deploy/operator/templates/hooks/crd-installer-job.yaml create mode 100644 deploy/operator/templates/hooks/rbac.yaml create mode 100644 deploy/operator/templates/openshift-owner-finalizers-rbac.yaml create mode 100644 deploy/operator/templates/openshift-scc-rbac.yaml create mode 100644 deploy/operator/templates/seaweedfs-operator-servicemonitor-role.yaml create mode 100644 deploy/operator/templates/telemetry-config.yaml create mode 100644 deploy/operator/templates/telemetry-validation.yaml create mode 100644 deploy/operator/templates/wandb-cr.yaml create mode 100644 deploy/operator/templates/wandb-operator-application-role.yaml create mode 100644 deploy/operator/templates/wandb-operator-clickhouse-role.yaml create mode 100644 deploy/operator/templates/wandb-operator-grafana-role.yaml create mode 100644 deploy/operator/templates/wandb-operator-mysql-role.yaml create mode 100644 deploy/operator/templates/wandb-operator-redis-role.yaml create mode 100644 deploy/operator/templates/wandb-operator-seaweedfs-role.yaml create mode 100644 deploy/operator/templates/wandb-operator-vm-role.yaml create mode 100644 deploy/operator/templates/wandb-operator-wandb-role.yaml create mode 100644 deploy/operator/templates/webhooks.yaml create mode 100644 deploy/operator/values.schema.json create mode 100644 deploy/operator/values.yaml create mode 100644 deploy/telemetry/Chart.yaml create mode 100644 deploy/telemetry/dashboards/wandb-application.json create mode 100644 deploy/telemetry/dashboards/wandb-field-investigation.json create mode 100644 deploy/telemetry/dashboards/wandb-managed-install-performance.json create mode 100644 deploy/telemetry/dashboards/wandb-telemetry-overview.json create mode 100644 deploy/telemetry/templates/_helpers.tpl create mode 100644 deploy/telemetry/templates/telemetry-alerting.yaml create mode 100644 deploy/telemetry/templates/telemetry-otlp-gateway.yaml create mode 100644 deploy/telemetry/templates/telemetry-scrapes.yaml create mode 100644 deploy/telemetry/templates/telemetry-ui.yaml create mode 100644 deploy/telemetry/templates/telemetry-victoria-core.yaml create mode 100644 deploy/telemetry/templates/validation.yaml create mode 100644 deploy/telemetry/values.schema.json create mode 100644 deploy/telemetry/values.yaml create mode 100644 docs/design/wandb_v2/infra_reconciliation.md create mode 100644 docs/design/wandb_v2/legacy_overrides.md create mode 100644 docs/design/wandb_v2/struct_usage_graph.md create mode 100644 docs/design/wandb_v2/struct_usage_mapping.md create mode 100644 docs/design/wandb_v2/tilt.md create mode 100644 docs/infra-connection-settings.md create mode 100644 docs/monitoring.md create mode 100644 docs/openshift.md create mode 100644 docs/plan-ingress-gateway-api.md create mode 100644 docs/releasing.md delete mode 100644 ginko.mk delete mode 100644 golangci.yaml create mode 100755 hack/scripts/kind-images-manager.sh create mode 100755 hack/scripts/list-crs.sh create mode 100755 hack/scripts/managed-connections-to-external.sh create mode 100755 hack/scripts/pull-server-manifest.sh create mode 100755 hack/scripts/setup_crc.sh create mode 100755 hack/scripts/setup_kind.sh create mode 100755 hack/scripts/teardown_kind.sh create mode 100755 hack/scripts/test-weave.py create mode 100755 hack/scripts/tilt-dev-clean.sh create mode 100755 hack/scripts/tilt-down-dev-clean.sh create mode 100755 hack/scripts/verify-custom-ca-e2e.sh create mode 100644 hack/testing-manifests/cert-manager/ca-certificate.yaml create mode 100644 hack/testing-manifests/cert-manager/ca-issuer.yaml create mode 100644 hack/testing-manifests/cert-manager/selfsigned-issuer.yaml delete mode 100644 hack/testing-manifests/minio/minio.yaml create mode 100644 hack/testing-manifests/seaweedfs/seaweedfs.yaml create mode 100644 hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml create mode 100644 hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/sizing.yaml create mode 100644 hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml create mode 100644 hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/sizing.yaml create mode 100644 hack/testing-manifests/telemetry/grafana-dev.yaml create mode 100644 hack/testing-manifests/telemetry/infra-metrics-dev.yaml create mode 100644 hack/testing-manifests/telemetry/kube-metrics-dev.yaml create mode 100644 hack/testing-manifests/telemetry/operator-metrics-dev.yaml create mode 100644 hack/testing-manifests/telemetry/victoria-dev.yaml create mode 100644 hack/testing-manifests/telemetry/wandb-otel-connection-dev.yaml create mode 100644 hack/testing-manifests/test-infra/Chart.yaml create mode 100644 hack/testing-manifests/test-infra/examples/wandb-external-v1.yaml create mode 100644 hack/testing-manifests/test-infra/examples/wandb-external-v2.yaml create mode 100644 hack/testing-manifests/test-infra/templates/_helpers.tpl create mode 100644 hack/testing-manifests/test-infra/templates/certificates.yaml create mode 100644 hack/testing-manifests/test-infra/templates/mysql-deployment.yaml create mode 100644 hack/testing-manifests/test-infra/templates/mysql-secret.yaml create mode 100644 hack/testing-manifests/test-infra/templates/mysql-service.yaml create mode 100644 hack/testing-manifests/test-infra/templates/redis-deployment.yaml create mode 100644 hack/testing-manifests/test-infra/templates/redis-secret.yaml create mode 100644 hack/testing-manifests/test-infra/templates/redis-service.yaml create mode 100644 hack/testing-manifests/test-infra/templates/seaweedfs-create-bucket-job.yaml create mode 100644 hack/testing-manifests/test-infra/templates/seaweedfs-deployment.yaml create mode 100644 hack/testing-manifests/test-infra/templates/seaweedfs-secret.yaml create mode 100644 hack/testing-manifests/test-infra/templates/seaweedfs-service.yaml create mode 100644 hack/testing-manifests/test-infra/values.yaml create mode 100644 hack/testing-manifests/wandb/.generated/.gitkeep create mode 100644 hack/testing-manifests/wandb/custom-ca-e2e/README.md create mode 100644 hack/testing-manifests/wandb/kustomize/base/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/base/wandb.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/disable-clickhouse/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/disable-clickhouse/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/disable-kafka/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/disable-kafka/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/disable-mysql/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/disable-mysql/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/disable-objectstore/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/disable-objectstore/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/disable-redis/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/disable-redis/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/external-clickhouse/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/external-clickhouse/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/external-mysql/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/external-mysql/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/external-objectstore/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/external-objectstore/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/external-redis/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/external-redis/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/license-file/.gitignore create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/license-file/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/networking-gateway-local/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/networking-gateway-local/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/networking-ingress-local/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/networking-ingress-local/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/purge-retention/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/purge-retention/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/server-version-0.78.0/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/server-version-0.78.0/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/size-micro/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/size-micro/patch.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/size-small/kustomization.yaml create mode 100644 hack/testing-manifests/wandb/kustomize/overlays/size-small/patch.yaml rename hack/testing-manifests/wandb/{default.yaml => wandb-default-v1.yaml} (50%) create mode 100644 hack/testing-manifests/wandb/wandb-dev-v2.yaml create mode 100644 hack/testing-manifests/wandb/wandb-legacy-overrides-v1.yaml create mode 100644 hack/tilt/wandbcr/main.go create mode 100644 hack/tilt/wandbcr/main_test.go create mode 100644 internal/controller/application_controller.go create mode 100644 internal/controller/application_controller_test.go create mode 100644 internal/controller/application_http_route_test.go create mode 100644 internal/controller/application_service_test.go create mode 100644 internal/controller/common/co_managed.go create mode 100644 internal/controller/common/common_suite_test.go create mode 100644 internal/controller/common/condition.go create mode 100644 internal/controller/common/condition_test.go create mode 100644 internal/controller/common/detach.go create mode 100644 internal/controller/common/labels.go create mode 100644 internal/controller/common/naming.go create mode 100644 internal/controller/common/naming_test.go create mode 100644 internal/controller/common/pods.go create mode 100644 internal/controller/common/resource.go create mode 100644 internal/controller/common/resource_test.go create mode 100644 internal/controller/common/retention.go create mode 100644 internal/controller/common/service.go create mode 100644 internal/controller/common/service_test.go create mode 100644 internal/controller/common/state.go create mode 100644 internal/controller/ctrlqueue/ctrl_state.go create mode 100644 internal/controller/ctrlqueue/ctrl_state_test.go create mode 100644 internal/controller/infra/external/clickhouse/clickhouse.go create mode 100644 internal/controller/infra/external/common.go create mode 100644 internal/controller/infra/external/mysql/mysql.go create mode 100644 internal/controller/infra/external/mysql/mysql_test.go create mode 100644 internal/controller/infra/external/objectstore/connection.go create mode 100644 internal/controller/infra/external/objectstore/connection_test.go create mode 100644 internal/controller/infra/external/objectstore/objectstore.go create mode 100644 internal/controller/infra/external/objectstore/objectstore_test.go create mode 100644 internal/controller/infra/external/objectstore/pathstyle.go create mode 100644 internal/controller/infra/external/objectstore/pathstyle_test.go create mode 100644 internal/controller/infra/external/redis/redis.go create mode 100644 internal/controller/infra/external/redis/redis_test.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/altinity_suite_test.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/conn.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/detach.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/image_test.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/keeper/keeper_suite_test.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/keeper/naming.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/keeper/read.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/keeper/read_test.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/keeper/spec.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/keeper/spec_test.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/keeper/values.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/keeper/write.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/naming.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/naming_test.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/objectstorage.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/objectstorage_test.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/purge.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/read.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/spec.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/spec_test.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/status.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/status_test.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/values.go create mode 100644 internal/controller/infra/managed/clickhouse/altinity/write.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/config.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/config_test.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/conn.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/detach.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/detach_test.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/image_test.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/naming.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/purge.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/read.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/spec.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/spec_test.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/status.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/values.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/write.go create mode 100644 internal/controller/infra/managed/kafka/bufstream/write_test.go create mode 100644 internal/controller/infra/managed/mysql/moco/conn.go create mode 100644 internal/controller/infra/managed/mysql/moco/detach.go create mode 100644 internal/controller/infra/managed/mysql/moco/image_test.go create mode 100644 internal/controller/infra/managed/mysql/moco/mysql_suite_test.go create mode 100644 internal/controller/infra/managed/mysql/moco/naming.go create mode 100644 internal/controller/infra/managed/mysql/moco/purge.go create mode 100644 internal/controller/infra/managed/mysql/moco/read.go create mode 100644 internal/controller/infra/managed/mysql/moco/spec.go create mode 100644 internal/controller/infra/managed/mysql/moco/spec_test.go create mode 100644 internal/controller/infra/managed/mysql/moco/status.go create mode 100644 internal/controller/infra/managed/mysql/moco/write.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/config.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/config_test.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/conn.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/detach.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/image_test.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/legacy_minio.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/naming.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/purge.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/read.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/read_test.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/seaweedfs_suite_test.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/spec.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/spec_test.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/status.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/status_test.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/write.go create mode 100644 internal/controller/infra/managed/objectstore/seaweedfs/write_test.go create mode 100644 internal/controller/infra/managed/redis/opstree/conn.go create mode 100644 internal/controller/infra/managed/redis/opstree/detach.go create mode 100644 internal/controller/infra/managed/redis/opstree/image_test.go create mode 100644 internal/controller/infra/managed/redis/opstree/naming.go create mode 100644 internal/controller/infra/managed/redis/opstree/opstree_suite_test.go create mode 100644 internal/controller/infra/managed/redis/opstree/purge.go create mode 100644 internal/controller/infra/managed/redis/opstree/read.go create mode 100644 internal/controller/infra/managed/redis/opstree/spec.go create mode 100644 internal/controller/infra/managed/redis/opstree/spec_test.go create mode 100644 internal/controller/infra/managed/redis/opstree/status.go create mode 100644 internal/controller/infra/managed/redis/opstree/values.go create mode 100644 internal/controller/infra/managed/redis/opstree/write.go create mode 100644 internal/controller/reconciler/cleanup_legacy_v1.go create mode 100644 internal/controller/reconciler/cleanup_legacy_v1_test.go create mode 100644 internal/controller/reconciler/clickhouse.go create mode 100644 internal/controller/reconciler/custom_ca.go create mode 100644 internal/controller/reconciler/custom_ca_test.go create mode 100644 internal/controller/reconciler/gateway.go create mode 100644 internal/controller/reconciler/infra_routes.go create mode 100644 internal/controller/reconciler/ingress.go create mode 100644 internal/controller/reconciler/ingress_test.go create mode 100644 internal/controller/reconciler/kafka.go create mode 100644 internal/controller/reconciler/legacy_overrides.go create mode 100644 internal/controller/reconciler/legacy_overrides_test.go create mode 100644 internal/controller/reconciler/manifest_order.go create mode 100644 internal/controller/reconciler/migrate_legacy.go create mode 100644 internal/controller/reconciler/migrate_legacy_test.go create mode 100644 internal/controller/reconciler/mysql.go create mode 100644 internal/controller/reconciler/networking_cleanup.go create mode 100644 internal/controller/reconciler/networking_route_builders_test.go create mode 100644 internal/controller/reconciler/objectstore.go create mode 100644 internal/controller/reconciler/oidc_env_test.go create mode 100644 internal/controller/reconciler/pods.go create mode 100644 internal/controller/reconciler/pods_instance_test.go create mode 100644 internal/controller/reconciler/pods_test.go create mode 100644 internal/controller/reconciler/probe_defaults.go create mode 100644 internal/controller/reconciler/probe_defaults_test.go create mode 100644 internal/controller/reconciler/rbac.go create mode 100644 internal/controller/reconciler/reconcile_v2.go create mode 100644 internal/controller/reconciler/reconcile_v2_infra_sizing_test.go create mode 100644 internal/controller/reconciler/reconcile_v2_objectstore_sizing_test.go create mode 100644 internal/controller/reconciler/reconcile_v2_sizing_test.go create mode 100644 internal/controller/reconciler/redis.go create mode 100644 internal/controller/reconciler/sizing.go create mode 100644 internal/controller/reconciler/status_update.go create mode 100644 internal/controller/reconciler/status_update_test.go create mode 100644 internal/controller/reconciler/telemetry_chart_test.go create mode 100644 internal/controller/reconciler/telemetry_config.go create mode 100644 internal/controller/reconciler/telemetry_configmap.go create mode 100644 internal/controller/reconciler/telemetry_secret.go create mode 100644 internal/controller/reconciler/telemetry_status.go create mode 100644 internal/controller/reconciler/telemetry_test.go create mode 100644 internal/controller/reconciler/v2_suite_test.go create mode 100644 internal/controller/weightsandbiases_controller_networking_test.go create mode 100644 internal/crdinstaller/apply.go create mode 100644 internal/crdinstaller/compose.go create mode 100644 internal/crdinstaller/compose_test.go create mode 100644 internal/crdinstaller/crds/clickhouse/clickhouse.altinity.com_clickhouseinstallations.yaml create mode 100644 internal/crdinstaller/crds/operator/apps.wandb.com_applications.yaml create mode 100644 internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml create mode 100644 internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redis.yaml create mode 100644 internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redisclusters.yaml create mode 100644 internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redisreplications.yaml create mode 100644 internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redissentinels.yaml create mode 100644 internal/crdinstaller/embed.go create mode 100644 internal/crdinstaller/render.go create mode 100644 internal/logx/attrs.go create mode 100644 internal/logx/context.go create mode 100644 internal/logx/handler.go create mode 100644 internal/logx/options.go create mode 100644 internal/logx/pretty.go create mode 100644 internal/logx/settings.go create mode 100755 internal/metrics/metrics.go create mode 100644 internal/metrics/metrics_test.go create mode 100644 internal/probes/probes.go create mode 100644 internal/webhook/v2/application_webhook.go create mode 100644 internal/webhook/v2/application_webhook_test.go create mode 100644 internal/webhook/v2/webhook_suite_test.go create mode 100644 internal/webhook/v2/weightsandbiases_defaulter_clickhouse_test.go create mode 100644 internal/webhook/v2/weightsandbiases_defaulter_kafka_test.go create mode 100644 internal/webhook/v2/weightsandbiases_defaulter_mysql_test.go create mode 100644 internal/webhook/v2/weightsandbiases_defaulter_objectstore_test.go create mode 100644 internal/webhook/v2/weightsandbiases_defaulter_redis_test.go create mode 100644 internal/webhook/v2/weightsandbiases_multi_instance_test.go create mode 100644 internal/webhook/v2/weightsandbiases_probe_defaults.go create mode 100644 internal/webhook/v2/weightsandbiases_probe_defaults_test.go create mode 100644 internal/webhook/v2/weightsandbiases_webhook.go create mode 100644 internal/webhook/v2/weightsandbiases_webhook_test.go create mode 100644 pkg/utils/coalesce.go create mode 100644 pkg/utils/coalesce_test.go create mode 100644 pkg/utils/connection_secrets.go create mode 100644 pkg/utils/gvk.go create mode 100644 pkg/utils/gvk_test.go create mode 100644 pkg/utils/maps.go create mode 100644 pkg/utils/maps_test.go create mode 100644 pkg/utils/openshift.go create mode 100644 pkg/utils/slices.go create mode 100644 pkg/utils/strings.go create mode 100644 pkg/utils/utils_suite_test.go create mode 100644 pkg/vendored/altinity-clickhouse/README.md create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/api_group.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/api_register.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/api_resources.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/api_version.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/doc.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_chk.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_cluster.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_configuration.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_replica.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_shard.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_spec.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_status.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/types.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/zz_generated.deepcopy.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/api_group.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/action_plan.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/api_register.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/api_resources.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/api_version.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/cr.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/doc.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/interface.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_attributes.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_chi.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_chi_templating.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_cleanup.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_cluster.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_cluster_secret.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_configuration_chi.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_configuration_chop.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_defaults.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_distributed_ddl.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_host.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_host_address.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_hosts_field.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_reconcile.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_reconcile_macros.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_reconcile_runtime.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_replica.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting_data_source.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting_scalar.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting_vector.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_name2key_converter_plain.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_name2key_converter_prefix.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_section.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_user.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_shard.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_spec.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_status.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_status_test.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_storage_management.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_target_selector.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_template_indexes.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_template_ref.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_templates.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_templates_list.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_volume_claim_template.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_zookeeper.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_zookeeper_node.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/types.go create mode 100644 pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/zz_generated.deepcopy.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/copy_cr_options.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/data_source.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/host_scope_address.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/id.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/int32.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/list.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/matchable.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/object_address.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/object_status.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/port.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/reconcile_attributes.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/status_options.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/string.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/string_bool.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/strings.go create mode 100644 pkg/vendored/altinity-clickhouse/common/types/tags.go create mode 100644 pkg/vendored/altinity-clickhouse/crds/clickhouse-operator-install-bundle.yaml create mode 100644 pkg/vendored/altinity-clickhouse/crds/clickhouse.altinity.com_clickhouseinstallations.yaml create mode 100644 pkg/vendored/altinity-clickhouse/deployment/affinity.go create mode 100644 pkg/vendored/altinity-clickhouse/deployment/env_vars.go create mode 100644 pkg/vendored/altinity-clickhouse/metrics/watched_resource.go create mode 100644 pkg/vendored/altinity-clickhouse/swversion/min_max.go create mode 100644 pkg/vendored/altinity-clickhouse/swversion/software_version.go create mode 100644 pkg/vendored/altinity-clickhouse/util/array.go create mode 100644 pkg/vendored/altinity-clickhouse/util/context.go create mode 100644 pkg/vendored/altinity-clickhouse/util/dir.go create mode 100644 pkg/vendored/altinity-clickhouse/util/dump.go create mode 100644 pkg/vendored/altinity-clickhouse/util/duration.go create mode 100644 pkg/vendored/altinity-clickhouse/util/fingerprint.go create mode 100644 pkg/vendored/altinity-clickhouse/util/fqdn.go create mode 100644 pkg/vendored/altinity-clickhouse/util/fs.go create mode 100644 pkg/vendored/altinity-clickhouse/util/hash.go create mode 100644 pkg/vendored/altinity-clickhouse/util/int.go create mode 100644 pkg/vendored/altinity-clickhouse/util/k8s.go create mode 100644 pkg/vendored/altinity-clickhouse/util/map.go create mode 100644 pkg/vendored/altinity-clickhouse/util/messagediff.go create mode 100644 pkg/vendored/altinity-clickhouse/util/print.go create mode 100644 pkg/vendored/altinity-clickhouse/util/prometheus.go create mode 100644 pkg/vendored/altinity-clickhouse/util/replacer.go create mode 100644 pkg/vendored/altinity-clickhouse/util/retry/retry.go create mode 100644 pkg/vendored/altinity-clickhouse/util/rfc1035.go create mode 100644 pkg/vendored/altinity-clickhouse/util/runtime/runtime.go create mode 100644 pkg/vendored/altinity-clickhouse/util/shell.go create mode 100644 pkg/vendored/altinity-clickhouse/util/slice.go create mode 100644 pkg/vendored/altinity-clickhouse/util/string.go create mode 100644 pkg/vendored/altinity-clickhouse/version/version.go create mode 100644 pkg/vendored/altinity-clickhouse/xml/xml.go create mode 100644 pkg/vendored/argo-rollouts/README.md create mode 100644 pkg/vendored/argo-rollouts/argoproj.io.rollouts/register.go create mode 100644 pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/analysis_types.go create mode 100644 pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/doc.go create mode 100644 pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/experiment_types.go create mode 100644 pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/register.go create mode 100755 pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/types.go create mode 100644 pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/types_test.go create mode 100644 pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/zz_generated.deepcopy.go create mode 100644 pkg/vendored/argo-rollouts/crds/analysis-run-crd.yaml create mode 100644 pkg/vendored/argo-rollouts/crds/analysis-template-crd.yaml create mode 100644 pkg/vendored/argo-rollouts/crds/cluster-analysis-template-crd.yaml create mode 100644 pkg/vendored/argo-rollouts/crds/experiment-crd.yaml create mode 100755 pkg/vendored/argo-rollouts/crds/rollout-crd.yaml create mode 100644 pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_backendtlspolicies.yaml create mode 100644 pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_gatewayclasses.yaml create mode 100644 pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_gateways.yaml create mode 100644 pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_grpcroutes.yaml create mode 100644 pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_httproutes.yaml create mode 100644 pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_listenersets.yaml create mode 100644 pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_referencegrants.yaml create mode 100644 pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_tlsroutes.yaml create mode 100644 pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_vap_safeupgrades.yaml create mode 100644 pkg/vendored/moco/crds/moco_crds.yaml create mode 100644 pkg/vendored/redis-operator/README.md create mode 100644 pkg/vendored/redis-operator/common/v1beta2/README.md create mode 100644 pkg/vendored/redis-operator/common/v1beta2/common_types.go create mode 100644 pkg/vendored/redis-operator/common/v1beta2/common_types_test.go create mode 100644 pkg/vendored/redis-operator/common/v1beta2/doc.go create mode 100644 pkg/vendored/redis-operator/common/v1beta2/zz_generated.deepcopy.go create mode 100644 pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redis.yaml create mode 100644 pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redisclusters.yaml create mode 100644 pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redisreplications.yaml create mode 100644 pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redissentinels.yaml create mode 100644 pkg/vendored/redis-operator/redis/v1beta2/README.md create mode 100644 pkg/vendored/redis-operator/redis/v1beta2/groupversion_info.go create mode 100644 pkg/vendored/redis-operator/redis/v1beta2/redis_conversion.go create mode 100644 pkg/vendored/redis-operator/redis/v1beta2/redis_types.go create mode 100644 pkg/vendored/redis-operator/redis/v1beta2/redis_webhook.go create mode 100644 pkg/vendored/redis-operator/redis/v1beta2/zz_generated.deepcopy.go create mode 100644 pkg/vendored/redis-operator/redisreplication/v1beta2/README.md create mode 100644 pkg/vendored/redis-operator/redisreplication/v1beta2/groupversion_info.go create mode 100644 pkg/vendored/redis-operator/redisreplication/v1beta2/redisreplication_conversion.go create mode 100644 pkg/vendored/redis-operator/redisreplication/v1beta2/redisreplication_types.go create mode 100644 pkg/vendored/redis-operator/redisreplication/v1beta2/redisreplication_webhook.go create mode 100644 pkg/vendored/redis-operator/redisreplication/v1beta2/zz_generated.deepcopy.go create mode 100644 pkg/vendored/redis-operator/redissentinel/v1beta2/README.md create mode 100644 pkg/vendored/redis-operator/redissentinel/v1beta2/groupversion_info.go create mode 100644 pkg/vendored/redis-operator/redissentinel/v1beta2/redissentinel_conversion.go create mode 100644 pkg/vendored/redis-operator/redissentinel/v1beta2/redissentinel_types.go create mode 100644 pkg/vendored/redis-operator/redissentinel/v1beta2/redissentinel_webhook.go create mode 100644 pkg/vendored/redis-operator/redissentinel/v1beta2/zz_generated.deepcopy.go create mode 100644 pkg/vendored/seaweedfs-operator/README.md create mode 100644 pkg/vendored/seaweedfs-operator/crds/seaweed.seaweedfs.com_seaweeds.yaml create mode 100644 pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/groupversion_info.go create mode 100644 pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/types.go create mode 100644 pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/vendored_helpers.go create mode 100644 pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/zz_generated.deepcopy.go create mode 100644 pkg/wandb/manifest/image_ref_test.go create mode 100644 pkg/wandb/manifest/load_manifest_from_files_test.go create mode 100644 pkg/wandb/manifest/manifest.go create mode 100644 pkg/wandb/manifest/manifest_decode_test.go create mode 100644 pkg/wandb/manifest/manifest_suite_test.go create mode 100644 test/e2e/retention_test.go delete mode 100644 tilt-settings.sample.json create mode 100644 tilt-settings.sample.star diff --git a/.claude/skills/convert-datadog-dashboard/SKILL.md b/.claude/skills/convert-datadog-dashboard/SKILL.md new file mode 100644 index 00000000..8d139ae6 --- /dev/null +++ b/.claude/skills/convert-datadog-dashboard/SKILL.md @@ -0,0 +1,173 @@ +--- +name: convert-datadog-dashboard +description: Convert a DataDog dashboard JSON into a Grafana dashboard JSON that fits the operator's telemetry stack (VictoriaMetrics datasources, wandb- UID, no templating). Trigger when the user provides DataDog dashboard JSON (inline or as a file path) and asks to port, convert, or recreate it as a Grafana dashboard, or when the user asks to add a new dashboard under deploy/telemetry/dashboards/ from a DataDog source. +--- + +# Convert DataDog dashboard → Grafana dashboard + +This skill ports a DataDog dashboard JSON into a Grafana dashboard JSON that matches the operator's existing telemetry stack. It does NOT edit the Helm wiring template — it prints a wiring reminder at the end so the user sees that change alongside the new dashboard file in a single diff. + +## Scope: per-CR only (refuses fleet dashboards) + +The operator's telemetry stack runs per-W&B-CR — each deployment has its own VictoriaMetrics and Grafana, and only sees its own data. This skill is intentionally narrow: it converts **per-service / per-component** DataDog dashboards (one tenant, one service) into a Grafana dashboard for that per-CR Grafana. + +It explicitly refuses **fleet / multi-tenant** DataDog dashboards (anything sharded across `$customer-ns`, `$tenant`, `$workspace`, or similar) — see Step 0 for the detection logic and refusal message. A converted fleet dashboard would show empty panels inside a single CR's Grafana because the cross-tenant data simply isn't there. Fleet observability needs a separate centrally-deployed Grafana federating across CRs and is out of scope here. + +## Inputs to collect + +If the user hasn't provided these, ask once via `AskUserQuestion`: + +1. **DataDog dashboard JSON** — accept as a file path, pasted content, or already-parsed object. Validate it contains both `title` and `widgets` keys before proceeding. +2. **Target component name** — kebab-case, single token describing the service or component this dashboard covers (e.g. `api-deep-dive`, `mysql-deep-dive`, `kafka-deep-dive`). This becomes both the UID suffix (`wandb-`) and the filename (`wandb-.json`). If the user gives a multi-word phrase, propose a slug and confirm. +3. **Overwrite policy** — if `deploy/telemetry/dashboards/wandb-.json` already exists, confirm before overwriting. + +## Procedure + +Run these steps sequentially. Don't parallelize — later steps assume the output of earlier ones. + +### Step 0 — Classify the DataDog source (per-CR vs. fleet) + +Before any conversion work, decide whether the source is per-CR (proceed) or fleet/multi-tenant (refuse). This guards against producing a dashboard that would show empty panels inside a per-CR Grafana. + +The source is **fleet/multi-tenant** if ANY of the following are true: + +1. `template_variables[]` contains an entry whose `name` (case-insensitive) matches: `customer-ns`, `customer_ns`, `customer`, `tenant`, `workspace`, `cluster`, `account`, `org`, `organization`, or starts with `excluded-` / `excluded_`. +2. Any widget query groups by `customer-ns`, `customer_ns`, `tenant`, or `workspace` (the grouping label IS a tenancy identifier). Note: grouping by `kube_namespace` is fine — within a single CR there can be legitimate multi-namespace views. +3. The dashboard `title` contains "Fleet", "MI" (multi-instance), "All Customers", "Tenancy", or "Cross-tenant" (case-insensitive substring match). + +If any of those fire, **stop** and print this exact refusal message (substitute the detected variables / labels into ``): + +``` +This DataDog dashboard appears to be fleet/multi-tenant — it references +tenancy variables () or groups by tenant labels. The operator's +telemetry stack runs per-W&B-CR (single-tenant), so a converted version +inside one CR's Grafana would never have the cross-tenant data it needs. + +Options: + 1. Pick a per-service / per-component DD dashboard instead. + 2. Strip tenancy variables and groupings from the DD source first, then re-run. + 3. Pursue fleet observability separately (a centrally-deployed Grafana + federating across CRs — out of scope for this skill). +``` + +Do not write any file. Do not proceed to Step 1. The user can either pick a different DD source or strip the tenancy bits from this one and re-run. + +If none of the rules fire, the source is per-CR — continue to Step 1. + +### Step 1 — Read the canonical references + +Read `deploy/telemetry/dashboards/wandb-telemetry-overview.json` and `deploy/telemetry/dashboards/wandb-mysql.json` to refresh the exact shape of the target JSON (especially `__inputs`, panel structures, and `gridPos` conventions). Do not skip this — schema 39 details change subtly across Grafana releases. + +### Step 2 — Build the skeleton + +Use this skeleton, filling in `` and the title: + +```json +{ + "__inputs": [ + { + "name": "DS_VICTORIAMETRICS", + "label": "VictoriaMetrics", + "type": "datasource", + "pluginId": "victoriametrics-metrics-datasource", + "pluginName": "VictoriaMetrics" + } + ], + "annotations": { "list": [] }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [], + "refresh": "30s", + "schemaVersion": 39, + "style": "dark", + "tags": ["wandb", "", "observability"], + "templating": { "list": [] }, + "time": { "from": "now-6h", "to": "now" }, + "timezone": "browser", + "title": "W&B ", + "uid": "wandb-", + "version": 1 +} +``` + +**Datasource inclusion rule:** add `DS_VICTORIALOGS` to `__inputs` only if at least one converted panel queries logs (DD `data_source: "logs"` or `data_source: "logs_pattern_stream"`). Add `DS_VICTORIATRACES` only if a panel references traces or you preserve a "Open Traces in Explore" link. Mirror the per-dashboard pattern: `wandb-mysql.json` declares Metrics + Logs only; `wandb-telemetry-overview.json` declares Metrics + Traces only. The Helm wiring (`deploy/telemetry/templates/telemetry-ui.yaml`) passes all three regardless, so declaring fewer is safe. + +### Step 3 — Walk the widget tree + +DataDog `widgets` is a flat array of objects, but `definition.type == "group"` widgets contain a nested `widgets` array. Emit a Grafana `row` panel per group, then recurse into its children. For each leaf widget look up the type in `references/widget-mapping.md` and build the matching Grafana panel. + +### Step 4 — Translate queries + +For each `definition.requests[].queries[]` and `definition.requests[].formulas[]`, apply the rules in `references/query-mapping.md`. Drop DD template variables (`$customer-ns`, `$env`, `$cloud_provider`, `$size`, etc.) from filter sets — do not map them to Grafana variables; we follow the no-templating convention. + +Per-query routing — substitutions take precedence over TODOs: + +1. **Syntactic translation** — apply the aggregator/grouping rewrite, suffix functions (`.as_rate`, `.rollup`, `.fill`), formula wrappers (`monotonic_diff`, `default_zero`, `per_second`, etc.), filter-token drops for `$`, and dot→underscore metric name conversion. This gives you the literal-translated metric name (e.g. DD `gcp.redis.clients.connected` → `gcp_redis_clients_connected`). +2. **Substitution check** — look the literal-translated metric name up in the "Known substitutions" table in `references/query-mapping.md`. + - **If found:** swap the metric expression with the substitution's RHS. Apply the label-rename rules (DD `pod_name` → cAdvisor `pod`, etc.) inside `by (...)` groupings and `{...}` filter sets, but ONLY when the RHS is a cAdvisor/kubelet/machine metric. Attach a panel description: `"Substituted from DataDog metric ''."` (so the swap is auditable). Done — do NOT also add a TODO description. + - **If not found:** continue to step 3. +3. **TODO fallback** — check whether the literal-translated metric's prefix is in the "Unscraped metric prefixes (TODO list)" in `references/query-mapping.md`. + - **If yes:** keep the literal-translated expression and attach this panel description: + ``` + TODO: metric '' is not currently scraped by the operator telemetry stack. Verify VictoriaMetrics has data before relying on this panel; add a scrape config in deploy/telemetry/templates/telemetry-scrapes.yaml if needed. + ``` + - **If no:** keep the literal-translated expression unchanged and add no description — the metric is presumably already in our scrape set under the same name (e.g. `mysql_global_status_threads_connected` is queryable as-is). + +Keep panels in place in all three cases — the user wants to see what existed in DD even when we can't query it yet. The audit description distinguishes a substituted panel from a pass-through panel. + +### Step 5 — Convert layout + +DataDog uses a 12-column grid (`{x, y, width, height}` on `widget.layout`). Grafana uses a 24-column grid (`gridPos: {x, y, w, h}`). Multiply DD `x` and `width` by 2; keep `y` and `height` as-is. Row panels span the full grid: `{ "h": 1, "w": 24, "x": 0, "y": }`. + +### Step 6 — Scrub sensitive content + +Apply every rule in `references/scrub-checklist.md` before writing. If any rule fails loudly (e.g. a hardcoded customer namespace survived substitution), stop and report the offending value — do not write the file. + +### Step 7 — Output validation + +Before writing, validate the in-memory JSON: + +- `uid == "wandb-"` and matches the planned filename +- Every panel `datasource.uid` is one of `${DS_VICTORIAMETRICS}`, `${DS_VICTORIALOGS}`, `${DS_VICTORIATRACES}` +- `tags` contains `"wandb"`, ``, `"observability"` +- `templating.list` is empty +- No string in any `expr` contains `$customer-ns`, `$env`, `$cloud_provider`, `$size`, `$kube_namespace`, `$kube_deployment`, `$kube_daemon_set`, `$kube_stateful_set`, `$image_tag`, `$kafka.topic.name`, `$helm_chart_name`, or `$helm_chart_version` + +### Step 8 — Write the file + +Write to `deploy/telemetry/dashboards/wandb-.json`. Use `jq -S .` (or equivalent) to keep field order deterministic if jq is available; otherwise use a stable JSON serializer with two-space indent so future diffs stay minimal. + +### Step 9 — Print the wiring reminder + +Output verbatim: + +``` +Dashboard written to deploy/telemetry/dashboards/wandb-.json + +Wire it into the Helm chart manually (this skill does not edit templates): + + 1. Open deploy/telemetry/templates/telemetry-ui.yaml + 2. Find the line beginning with: {{- range $component := list "operator" "api" "mysql" + 3. Add "" to the list (any position is fine) + 4. Verify with: helm template deploy/telemetry/ --set mode=full | grep "uid: wandb-" + (--set mode=full is required — the chart's UI templates are gated on telemetry.uiEnabled, which is true only when mode=full. Without the flag, the command renders nothing and the wiring will look broken even when it isn't.) + +Then visually inspect the dashboard in Grafana and address any TODO descriptions left on panels whose source metrics aren't yet scraped. +``` + +Confirm the `range` line exists before printing — if `grep -n 'range \$component' deploy/telemetry/templates/telemetry-ui.yaml` returns nothing, the template has changed and the reminder needs to be re-derived. + +## When NOT to use this skill + +- The user has a Grafana dashboard already and just wants to edit it — use the file directly. +- The user wants to add panels to an existing dashboard — open the JSON and edit. +- The user wants a new dashboard from scratch (no DD source) — write the Grafana JSON directly, following the skeleton above. +- The DD source uses log analytics, security signals, RUM, or APM features that have no metric/log equivalent in this stack — flag this up front and ask if a partial port is acceptable. + +## References (load on demand) + +- `references/widget-mapping.md` — DD widget type → Grafana panel type table +- `references/query-mapping.md` — DD query function → MetricsQL rules and the unscraped-prefix list +- `references/scrub-checklist.md` — sensitive-content patterns diff --git a/.claude/skills/convert-datadog-dashboard/references/query-mapping.md b/.claude/skills/convert-datadog-dashboard/references/query-mapping.md new file mode 100644 index 00000000..44f56813 --- /dev/null +++ b/.claude/skills/convert-datadog-dashboard/references/query-mapping.md @@ -0,0 +1,172 @@ +# DataDog query → MetricsQL/PromQL translation + +VictoriaMetrics speaks MetricsQL, a PromQL superset. Most translations below produce valid PromQL; a few use MetricsQL-only constructs where they're cleaner. The operator's stack accepts both. + +## Aggregator + grouping + +DataDog: `:{} by {}` +MetricsQL: ` by () ({})` + +Mappings: + +| DD aggregator | MetricsQL | +|---|---| +| `max:` | `max by () (...)` | +| `min:` | `min by () (...)` | +| `sum:` | `sum by () (...)` | +| `avg:` | `avg by () (...)` | + +If no `by {}` clause, drop the `by ()` — emit a scalar aggregate. + +## Suffix functions (chained with `.`) + +| DD suffix | MetricsQL | +|---|---| +| `.as_count()` | Drop. Add a TODO description if the underlying metric is clearly a gauge being miscoerced. | +| `.as_rate()` | Wrap whole expression: `rate([1m])` | +| `.rollup(max)` | `max_over_time([1m])` | +| `.rollup(max, 60)` | `max_over_time([60s])` | +| `.rollup(min, N)` | `min_over_time([Ns])` | +| `.rollup(avg, N)` | `avg_over_time([Ns])` | +| `.rollup(sum, N)` | `sum_over_time([Ns])` | +| `.fill(zero)` | ` or vector(0)` | +| `.fill(linear)` | Drop; MetricsQL has `interpolate()` but it changes semantics — leave a TODO if precision matters. | + +## Wrapper functions used in `formulas[].formula` + +| DD | MetricsQL | +|---|---| +| `monotonic_diff(q)` | `increase([1m])` — leave a TODO noting the 1m window assumption | +| `default_zero(q)` | `() or vector(0)` | +| `exclude_null(q)` | Drop the wrapper. MetricsQL omits null series by default. | +| `per_second(q)` | `rate([1m])` | +| `log2(q)` | `log2()` (MetricsQL; not standard PromQL — flag if portability matters) | +| `cutoff_max(q, N)` | `clamp_max(, N)` | +| `cutoff_min(q, N)` | `clamp_min(, N)` | +| `abs(q)` | `abs()` | +| `hour_before(q)` / `day_before(q)` / `week_before(q)` | ` offset 1h` / `offset 24h` / `offset 7d` | +| `derivative(q)` | `deriv([1m])` | +| `timeshift(q, -3600)` | ` offset 1h` | +| Arithmetic on multiple queries (`query1 + query2`) | Substitute each `queryN` with its translated expression, keep the operator. | + +## Filter substitution + +DD filter sets look like `{$customer-ns,$env,$cloud_provider,key:value,!exclude:value}`. Apply in order: + +1. Drop any token starting with `$` — those are DD template variables and we don't use templating. +2. Convert `key:value` → `key="value"`. +3. Convert `!key:value` → `key!="value"`. +4. Convert `key:*` → drop the token (PromQL has no glob; `*` matched everything in DD). +5. If after all drops the filter set is empty, drop the braces entirely. + +Examples: + +- DD: `aws.rds.cpuutilization{$customer-ns,$env,$cloud_provider} by {customer-ns}` + → `max by (customer_ns) (aws_rds_cpuutilization)` + (note: also dotted-prefix → underscore conversion, below) + +- DD: `kubernetes.cpu.usage.total{$env, $customer-ns, !$excluded-customer-ns, container_name:bufstream} by {customer-ns,pod_name}` + → `max by (customer_ns, pod_name) (kubernetes_cpu_usage_total{container_name="bufstream"})` + +## Metric name conversion + +DD metric names use dots: `aws.rds.cpuutilization`. PromQL and most Prometheus exporters use underscores: `aws_rds_cpuutilization`. Always convert dots to underscores when emitting the expression. Preserve the metric name verbatim otherwise — do not invent a "more correct" name; the user can rename in a follow-up. + +Label names with dots (e.g. `kafka.topic.name`, `kafka.consumer.group.id`) follow the same rule when they appear in filter expressions: `kafka_topic_name`, `kafka_consumer_group_id`. + +## Known substitutions (DD → MetricsQL) + +When the translated DD metric matches one on the left, replace the metric expression in the output with the corresponding MetricsQL on the right. This produces queryable panels for cross-cloud and DD-wrapped-k8s metrics that have clear self-hosted equivalents in our scrape set. + +Substitutions are consulted **before** the TODO list (next section). If a metric is in both, substitution wins — see SKILL.md Step 4 for the ordering. + +**Confidence bar:** every row below has been verified semantically equivalent in units and meaning. Do not add rows where the LHS and RHS differ in scale, unit, or what they actually measure (e.g. CFS throttled *periods* vs throttled *seconds*) — leave those in the TODO list so the user sees the gap. + +| DD metric (literal-translated form) | MetricsQL substitution | +|---|---| +| `aws_rds_cpuutilization`, `azure_dbformysql_flexibleservers_cpu_percent`, `gcp_cloudsql_database_cpu_utilization` | `100 * rate(container_cpu_usage_seconds_total{container=~"mysqld\|pxc"}[$__rate_interval])` | +| `aws_rds_database_connections`, `azure_dbformysql_flexibleservers_total_connections`, `gcp_cloudsql_database_mysql_connections_count` | `mysql_global_status_threads_connected` | +| `aws_elasticache_database_memory_usage_percentage`, `azure_cache_redis_usedmemorypercentage`, `gcp_redis_stats_memory_usage_ratio` | `100 * redis_memory_used_bytes / redis_memory_max_bytes` | +| `aws_elasticache_curr_connections`, `azure_cache_redis_connectedclients`, `gcp_redis_clients_connected` | `redis_connected_clients` | +| `kubernetes_cpu_usage_total` | `rate(container_cpu_usage_seconds_total[$__rate_interval])` — apply label-rename rules below | +| `kubernetes_memory_usage` | `container_memory_working_set_bytes` — apply label-rename rules below | +| `kubernetes_network_rx_bytes` | `rate(container_network_receive_bytes_total[$__rate_interval])` — apply label-rename rules below | +| `kubernetes_network_tx_bytes` | `rate(container_network_transmit_bytes_total[$__rate_interval])` — apply label-rename rules below | +| `kubernetes_state_node_cpu_capacity` | `machine_cpu_cores` | + +### Label-rename rules + +Apply these label renames only when the substitution's RHS metric is `container_*`, `machine_*`, or `kubelet_*`. cAdvisor/kubelet use different label names than DD's `kubernetes.*` integration. + +- DD label `pod_name` → cAdvisor label `pod` +- DD label `container_name` → cAdvisor label `container` +- DD label `kube_namespace` → cAdvisor label `namespace` + +Apply the rename in both directions: inside `by (...)` groupings AND inside `{label="value"}` filter sets. Do NOT rename labels when the RHS is a non-cAdvisor metric (e.g. `mysql_*`, `redis_*`) — those exporters use their own label conventions. + +### Audit description + +When a substitution fires, attach a panel description to make the swap auditable: + +``` +Substituted from DataDog metric ''. +``` + +If the panel already has a description (from the widget mapping or scrub), append a new paragraph rather than overwriting. + +## Unscraped metric prefixes (TODO list) + +If a translated expression's metric prefix matches one of these AND it didn't match a substitution above, attach the TODO description to the panel (see SKILL.md Step 4). Verify this list against `deploy/telemetry/templates/telemetry-scrapes.yaml` at conversion time — the scrape set may have grown. + +Currently NOT scraped by `deploy/telemetry/templates/telemetry-scrapes.yaml`: + +- `aws_rds_*` — AWS RDS via CloudWatch (DD integration only) +- `aws_elasticache_*` — AWS ElastiCache via CloudWatch +- `aws_ec2_*` — AWS EC2 instance metrics +- `aws_autoscaling_*` — AWS Auto Scaling Groups +- `aws_vpc_*` — AWS VPC metrics +- `azure_dbformysql_flexibleservers_*` — Azure Database for MySQL Flexible Servers +- `azure_cache_redis_*` — Azure Cache for Redis +- `gcp_cloudsql_*` — GCP Cloud SQL +- `gcp_redis_*` — GCP Memorystore for Redis +- `bufstream_*` — Bufstream (DD-only OTel collection path; not via VMPodScrape today) +- `helm_release` — Helm release status (no exporter scraped today) +- `terraform_workspace_*` — Terraform Cloud workspace status (DD-only) +- `oom_kill_*` — DD OOM Kill check +- `kubernetes_*` (DD-style, only the ones NOT in the substitutions table above) — DD's wrapped kubernetes.* metrics. The substitutions table handles `kubernetes_cpu_usage_total`, `kubernetes_memory_usage`, `kubernetes_network_rx_bytes`, `kubernetes_network_tx_bytes` cleanly. Everything else under this prefix (e.g. `kubernetes_cpu_requests`, `kubernetes_cpu_limits`, `kubernetes_memory_requests`, `kubernetes_memory_limits`, `kubernetes_containers_restarts`, `kubernetes_pods_running`, `kubernetes_cpu_cfs_throttled_periods`) requires kube-state-metrics, which we don't currently scrape — TODO. +- `kubernetes_state_*` — DD's wrapped kube-state-metrics. We do not run kube-state-metrics in this stack today (`kubernetes_state_node_cpu_capacity` is the one exception covered by the substitutions table → `machine_cpu_cores`). Everything else stays TODO until kube-state-metrics is added to the scrape set. + +Currently scraped (rough prefixes the user can rely on): + +- `up`, `scrape_*`, `process_*`, `go_*` — always present +- `container_*`, `machine_*`, `cadvisor_*` — from kubelet/cadvisor scrape +- `kubelet_*` — from kubelet scrape +- `controller_runtime_*`, `workqueue_*`, `rest_client_*` — operator controller metrics +- `mysql_*`, `mysql_global_status_*`, `mysql_global_variables_*` — mysql exporter / mysqld +- `kafka_*`, `strimzi_*`, `kafka_server_*` — Strimzi-managed Kafka brokers +- `minio_*` — MinIO tenant metrics +- `redis_*` — redis_exporter on standalone Redis +- `ClickHouse*` (uppercase prefix is correct for ClickHouse native metrics endpoint), plus `chi_*` from the operator +- `grafana_*`, `vm_*` — Grafana operator, VictoriaMetrics operator + +## Cross-cloud "first non-null" patterns + +DD dashboards often define multiple queries (one per CSP) and `formulas` that just list them as alternatives. After dropping the cloud-managed-service metrics that aren't scraped, you may be left with a single self-hosted-metric query (e.g. `kubernetes.cpu.usage.total` for the self-hosted Redis on k8s). Prefer keeping the self-hosted query as the panel's primary target and dropping the cloud-managed alternatives — but keep them in the panel description as comments so a future scrape-config addition can re-enable them: + +``` +"description": "TODO: cloud-managed alternatives (currently not scraped): aws_elasticache_cpuutilization, azure_cache_redis_server_load, gcp_redis_stats_cpu_utilization" +``` + +## Examples + +DD: `max:gcp.cloudsql.database.cpu.utilization{$customer-ns,$env,$cloud_provider} by {customer-ns}.rollup(max)` +→ `max_over_time(max by (customer_ns) (gcp_cloudsql_database_cpu_utilization)[1m])` +→ panel description: TODO unscraped `gcp_cloudsql_*` + +DD: `monotonic_diff(sum:kubernetes.containers.restarts{$env,$customer-ns} by {customer-ns})` +→ `increase(sum by (customer_ns) (kubernetes_containers_restarts)[1m])` +→ panel description: TODO unscraped `kubernetes_*` (DD-style); window assumption 1m + +DD: `sum:bufstream.kafka.produce.bytes{$customer-ns,$env,$cloud_provider,$size,$kafka.topic.name} by {customer-ns,kafka.topic.name,kafka.topic.partition}.as_count()` +→ `sum by (customer_ns, kafka_topic_name, kafka_topic_partition) (bufstream_kafka_produce_bytes)` +→ panel description: TODO unscraped `bufstream_*`; `.as_count()` dropped diff --git a/.claude/skills/convert-datadog-dashboard/references/scrub-checklist.md b/.claude/skills/convert-datadog-dashboard/references/scrub-checklist.md new file mode 100644 index 00000000..884f8135 --- /dev/null +++ b/.claude/skills/convert-datadog-dashboard/references/scrub-checklist.md @@ -0,0 +1,74 @@ +# Sensitive-content scrub checklist + +Run every rule below before writing the dashboard JSON. Some rules "strip" (modify content silently), some "fail loudly" (stop the conversion and surface the problem to the user). Default to failing loudly — silent stripping is for patterns that are obviously safe to drop (DD-internal URLs in notes). + +## Always-strip patterns (silently scrub) + +These appear in `note`/`text` widget content, panel descriptions, and link targets. Strip them; if the field becomes empty after stripping, drop the field (or the whole panel for notes). + +- Any URL matching `app\.datadoghq\.(com|eu|us\d+)` — DD-internal dashboard links won't resolve outside DD +- Any path matching `/dash/integration/\d+` — DD integration dashboard IDs +- Any path matching `/dashboard/[a-z0-9-]+` when host is `*.datadoghq.*` — DD dashboard slugs +- Markdown link wrappers around stripped URLs: if the link text remains meaningful (e.g. "Kubernetes Pods Overview dashboard"), keep the text and drop just the URL; if not, drop the whole link + +## Always-strip from DD template variable defaults + +Do not carry DD `template_variables[].defaults` into the Grafana JSON. Specifically: + +- `template_variables[].defaults` may list real customer namespace names — never preserve these in any form (not as a comment, not as a default filter, not as a panel description) +- The `excluded-customer-ns` variable in the example DD source lists names like `wandb-annirudh`, `wandb-zalando`, `wb-sdd-4020c451`, `wandb-fe-crew`, `wandb-mademoiselle`, `wandb-gcpdaniel`, and various `wandb-*-perf-*` tenants. These are sensitive — they identify specific customers and internal tenants. They must NOT appear anywhere in the output. + +Since the converted dashboard uses no templating, the right behavior is to drop the entire `template_variables` array — but the literal customer names may still appear inline in `widget.definition.requests[].queries[].query` strings (e.g. as part of `customer-ns:wandb-foo` filter tokens). Those need fail-loudly handling (next section). + +## Fail-loudly patterns (stop the conversion) + +If any of these survive substitution and would appear in the final JSON, stop and report the offending value. Do not write the file. + +- Any hardcoded customer namespace literal in a query filter, alias, title, or formula. Detect with: regex match against the names listed above, plus any string matching `(wandb|wb)-[a-z0-9]+` that ALSO appears in the DD source's `template_variables` defaults (this catches customer-ns names you might not have seen before — if a name appears in a defaults list AND inline in a query, that's a customer identifier). +- Internal corporate hostnames matching `*.internal`, `*.corp.*`, or `*.private.*` +- Any IP address in private ranges (`10.*`, `172.16-31.*`, `192.168.*`) hardcoded in a query or note +- Any string matching `(eks|gke|aks)_cluster.name:[^,}]+` with a non-empty cluster name — cluster names often double as tenant identifiers +- Bearer-style tokens, API keys: anything matching `[A-Za-z0-9]{32,}` that appears in a `link.url` or note content (likely a session token or API key embedded by mistake) + +When you fail loudly, surface the panel title, the field that contains the offending value, and a snippet of the surrounding text. Example output: + +``` +SCRUB CHECK FAILED: panel "DB CPU Utilization" target A expression contains "customer-ns:wandb-annirudh". +Source DataDog query: max:gcp.cloudsql.database.cpu.utilization{...,customer-ns:wandb-annirudh} +This is a customer-identifying namespace literal and cannot be written to the dashboard JSON. +Resolution: edit the DD source to remove the hardcoded customer-ns filter, or confirm explicitly that this customer name is safe to publish. +``` + +## DD data sources that drop the panel + +Some DD query datasources have no equivalent in this stack. Drop the panel and emit a `text` panel at the same gridPos noting what was removed: + +- `data_source: "events"` — DD event search; no equivalent +- `data_source: "rum"` — Real User Monitoring; no equivalent +- `data_source: "security_signals"` — DD security; no equivalent +- `data_source: "audit"` — DD audit logs; no equivalent +- `data_source: "dataset"` with a base64 `dataset_id` — DD-internal DDSQL warehouse query; cannot be translated + +Replacement `text` panel: + +```json +{ + "type": "text", + "title": "", + "gridPos": { ...same as source... }, + "options": { + "mode": "markdown", + "content": "Original DD widget queried a `` source that has no equivalent in this stack. Rewrite manually or drop in a follow-up." + } +} +``` + +## Self-test before writing + +After all transformations, run these checks against the in-memory JSON: + +1. `grep -i "datadoghq" ` → no matches +2. `grep -iE "(wandb|wb)-[a-z0-9-]+" | grep -v '"wandb-"' | grep -v '"wandb"'` → no matches (the dashboard's own UID and the `wandb` tag are allowed) +3. `grep -E "\\$[a-z][a-z_-]+" ` → no matches outside `${DS_VICTORIAMETRICS}`, `${DS_VICTORIALOGS}`, `${DS_VICTORIATRACES}`, `$__rate_interval`, `$__interval` (these are Grafana built-ins and are fine) + +If any check fails, stop and surface the matching line. diff --git a/.claude/skills/convert-datadog-dashboard/references/widget-mapping.md b/.claude/skills/convert-datadog-dashboard/references/widget-mapping.md new file mode 100644 index 00000000..2e42f07b --- /dev/null +++ b/.claude/skills/convert-datadog-dashboard/references/widget-mapping.md @@ -0,0 +1,87 @@ +# DataDog widget → Grafana panel mapping + +Each row tells you which Grafana `type` to emit and the field-level translations that matter. Field names not listed should be dropped — Grafana defaults are fine. + +## Type lookup + +| DataDog `definition.type` | Grafana panel `type` | Notes | +|---|---|---| +| `timeseries` | `timeseries` | Map `requests[].queries[]` → `targets[]` (one per query) using refIds A, B, C…; map `requests[].formulas[]` → additional `targets[]` with `expr` built from the formula after query substitution. | +| `toplist` | `bargauge` | Drop `conditional_formats` (use Grafana thresholds via `fieldConfig.defaults.thresholds` instead, only if the user wants them). Map `sort.order_by[0].order` → `options.sortBy`. | +| `query_table` | `table` | Per-formula `cell_display_mode: "bar"` → field override with `custom.cellOptions.type: "gauge"`. | +| `treemap` | `barchart` | No native treemap in core Grafana. Set `options.orientation: "horizontal"` and add a panel description: `"Source DD widget was a treemap; rendered as a horizontal bar chart."`. | +| `sunburst` | `piechart` | Set `options.displayMode: "pie"`, `options.legend.displayMode: "table"`. Sunburst's nested groupings flatten — emit only the top group. | +| `distribution` | `histogram` | Single-formula histograms only. For multi-formula distributions, keep `histogram` but add a TODO description listing the dropped formulas. | +| `note` | `text` | Set `options.mode: "markdown"`, `options.content` to the scrubbed note text. Run scrub-checklist BEFORE writing. If content becomes empty after scrubbing, drop the panel entirely. | +| `list_stream` | `logs` | Datasource → `${DS_VICTORIALOGS}`. DD log-pattern stream queries don't map cleanly — emit a `logs` panel with an empty query and a TODO description: `"Source DD widget queried ; rewrite as a LogsQL expression."`. | +| `group` | `row` | Emit `{ "type": "row", "title": , "gridPos": { "h": 1, "w": 24, "x": 0, "y": }, "collapsed": false }`, then recurse into `definition.widgets`. Increment the outer `y` cursor past the row's children. | +| `manage_status`, `alert_graph`, `service_summary`, `service_map`, `slo`, `slo_list`, `geomap`, `image`, `iframe`, `free_text`, `event_stream`, `event_timeline`, `change`, `topology_map`, `funnel`, `wildcard` | (skip with note) | No reasonable equivalent in this stack. Emit a `text` panel at the same gridPos with `options.content: "Source DD widget type '' has no Grafana equivalent in this stack. Original title: "`. | + +## Common panel structure (apply to all metric panels) + +```json +{ + "type": "<grafana_type>", + "title": "<dd_title>", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { "h": <h>, "w": <w*2>, "x": <x*2>, "y": <y> }, + "targets": [ + { "expr": "<translated_query>", "refId": "A", "legendFormat": "<alias_or_empty>" } + ], + "fieldConfig": { "defaults": {}, "overrides": [] }, + "options": {} +} +``` + +`legendFormat` comes from DD `formulas[].alias` when present. If a formula has no alias, omit `legendFormat`. + +## Per-type field details + +### `timeseries` + +- `definition.display_type: "line"` → leave `fieldConfig.defaults.custom` empty (line is the default) +- `definition.display_type: "area"` → set `fieldConfig.defaults.custom.fillOpacity: 30` +- `definition.display_type: "bars"` → set `fieldConfig.defaults.custom.drawStyle: "bars"` +- `definition.show_legend: true` → set `options.legend.showLegend: true` (default), pull columns from `legend_columns` +- `definition.yaxis.min: "0"` → set `fieldConfig.defaults.min: 0` +- `definition.yaxis.scale: "log"` → `fieldConfig.defaults.custom.scaleDistribution: { "type": "log", "log": 10 }` +- `definition.yaxis.scale: "sqrt"` → no clean Grafana equivalent; leave linear and add a panel description + +### `query_table` + +- For each formula with `cell_display_mode: "bar"`, emit a per-field override: + ```json + { + "matcher": { "id": "byName", "options": "<formula_alias_or_value>" }, + "properties": [ + { "id": "custom.cellOptions", "value": { "type": "gauge" } } + ] + } + ``` +- DD `query_table` with `data_source: "dataset"` (DDSQL queries against DD's internal warehouse) — drop the panel entirely with a TODO note in a `text` panel at the same gridPos; the base64 dataset IDs are DD-internal and won't translate. + +### `note` + +- `definition.background_color` → ignore (Grafana text panels don't theme per-panel cleanly) +- `definition.content` → run through scrub checklist, then assign to `options.content` with `options.mode: "markdown"` +- If content becomes empty or contains only stripped patterns, drop the panel + +### `toplist` + +- `definition.requests[].sort.count` → `options.maxItems` (cap at 50 even if DD specified higher) +- `definition.requests[].sort.order_by[0].order: "desc"` → `options.sortBy: "Last *"`, `options.sortDir: "desc"` + +### `group` + +- Always emit `collapsed: false` (matches existing wandb-* dashboards) +- The row's children inherit the same y-cursor accounting as top-level panels +- DD `background_color` on the group → ignore + +## Layout coordinates + +DD `widget.layout = {x, y, width, height}` on a 12-column grid → Grafana `gridPos = {x: x*2, y: y, w: width*2, h: height}` on a 24-column grid. + +Group children carry their own local layout coordinates that DD already computes relative to the group's origin. Treat them as already-absolute within Grafana's grid (DD does the same — verify by inspecting `wandb-telemetry-overview` DD source if you have it). diff --git a/.github/workflows/chart-validation.yaml b/.github/workflows/chart-validation.yaml new file mode 100644 index 00000000..6d406a76 --- /dev/null +++ b/.github/workflows/chart-validation.yaml @@ -0,0 +1,71 @@ +name: Chart Validation + +on: + push: + branches: [v2, main] + pull_request: + branches: [v2, main] + +jobs: + chart-validation: + name: Chart Validation + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Install Helm + uses: azure/setup-helm@bf6a7d304bc2fdb57e0331155b7ebf2c504acf0a # v4 + with: + version: v3.19.0 + + - name: Install chart-testing + uses: helm/chart-testing-action@e6669bcd63d7cb57cb4380c33043eebe5d111992 # v2.6.1 + with: + version: v3.14.0 + + - name: Resolve chart dependencies + run: | + helm repo add ci-wandb https://charts.wandb.ai/ + helm repo add ci-moco https://cybozu-go.github.io/moco/ + helm repo add ci-ot-container-kit https://ot-container-kit.github.io/helm-charts + helm repo add ci-seaweedfs https://seaweedfs.github.io/seaweedfs-operator/ + helm repo add ci-prometheus-community https://prometheus-community.github.io/helm-charts + helm repo add ci-altinity https://helm.altinity.com + helm repo add ci-victoria-metrics https://victoriametrics.github.io/helm-charts/ + helm repo add ci-grafana https://grafana.github.io/helm-charts + helm dependency build deploy/operator + git diff --exit-code deploy/operator/Chart.lock + + - name: Run chart-testing + run: ct lint --all --config deploy/ct.yaml + + - name: Validate values schema + run: | + set -euo pipefail + helm lint --strict deploy/operator + for profile in deploy/operator/profiles/*.yaml; do + extra_args=() + if [[ "${profile}" == *telemetry-forward.yaml ]]; then + extra_args+=(--set-string telemetry.forwarding.otlp.endpoint=https://example.invalid:4317) + fi + helm lint --strict deploy/operator --values "${profile}" "${extra_args[@]}" + done + + - name: Render representative configurations + run: | + set -euo pipefail + helm template wandb-operator deploy/operator \ + --namespace wandb-operators \ + --include-crds >/dev/null + for profile in deploy/operator/profiles/*.yaml; do + extra_args=() + if [[ "${profile}" == *telemetry-forward.yaml ]]; then + extra_args+=(--set-string telemetry.forwarding.otlp.endpoint=https://example.invalid:4317) + fi + helm template wandb-operator deploy/operator \ + --namespace wandb-operators \ + --include-crds \ + --values "${profile}" \ + "${extra_args[@]}" >/dev/null + done diff --git a/.github/workflows/docker-build-scan.yml b/.github/workflows/docker-build-scan.yml index 4dd26199..7f74719c 100644 --- a/.github/workflows/docker-build-scan.yml +++ b/.github/workflows/docker-build-scan.yml @@ -2,9 +2,9 @@ name: Docker Build and Security Scan on: push: - branches: [v1] + branches: [v2, main] pull_request: - branches: [v1] + branches: [v2, main] jobs: build-and-scan: @@ -17,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Build Docker image - uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . push: false @@ -25,3 +25,13 @@ jobs: tags: wandb/operator:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@c07df6fec6fa692e6fd1200d50aaa1fdd66f03c8 # master + with: + image-ref: wandb/operator:${{ github.sha }} + format: "table" + exit-code: "1" + ignore-unfixed: true + vuln-type: "os,library" + severity: "HIGH,CRITICAL" diff --git a/.github/workflows/internal-chart-publish.yaml b/.github/workflows/internal-chart-publish.yaml index 195bc03c..1d05842e 100644 --- a/.github/workflows/internal-chart-publish.yaml +++ b/.github/workflows/internal-chart-publish.yaml @@ -5,14 +5,88 @@ on: jobs: release: - name: Release + name: Publish prerelease chart runs-on: ubuntu-latest permissions: - contents: 'read' - id-token: 'write' + contents: read + id-token: write + env: + CHART_REPOSITORY: us-docker.pkg.dev/wandb-production/public/wandb/charts steps: - name: Checkout - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - persist-credentials: false fetch-depth: 0 + persist-credentials: false + + - name: Validate prerelease chart version + id: chart + shell: bash + run: | + set -euo pipefail + version="$(awk '$1 == "version:" { print $2; exit }' deploy/operator/Chart.yaml | tr -d '\"')" + if [[ ! "${version}" =~ ^2\.[0-9]+\.[0-9]+-[0-9A-Za-z][0-9A-Za-z.-]*$ ]]; then + echo "Internal chart publishing requires a v2 prerelease version; got ${version}" >&2 + exit 1 + fi + echo "version=${version}" >> "${GITHUB_OUTPUT}" + + - name: Install Helm + uses: azure/setup-helm@bf6a7d304bc2fdb57e0331155b7ebf2c504acf0a # v4 + with: + version: v3.19.0 + + - name: Install chart-testing + uses: helm/chart-testing-action@e6669bcd63d7cb57cb4380c33043eebe5d111992 # v2.6.1 + with: + version: v3.14.0 + + - name: Lint charts + run: ct lint --all --config deploy/ct.yaml + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 + + - id: auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3 + with: + create_credentials_file: 'true' + token_format: access_token + project_id: wandb-production + workload_identity_provider: ${{ secrets.CI_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.CI_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }} + + - name: Authorize Docker for Artifact Registry + run: gcloud auth configure-docker us-docker.pkg.dev --quiet + + - name: Reject existing chart version + env: + VERSION: ${{ steps.chart.outputs.version }} + shell: bash + run: | + set -euo pipefail + artifact="${CHART_REPOSITORY}/operator:${VERSION}" + set +e + output="$(gcloud artifacts docker images describe "${artifact}" --format='value(image_summary.digest)' 2>&1)" + status=$? + set -e + if [[ ${status} -eq 0 ]]; then + echo "Refusing to overwrite existing chart ${artifact}" >&2 + exit 1 + fi + if ! grep -Eqi 'NOT_FOUND|not found' <<< "${output}"; then + echo "Could not safely determine whether ${artifact} exists:" >&2 + echo "${output}" >&2 + exit 1 + fi + + - name: Package and publish prerelease chart + env: + VERSION: ${{ steps.chart.outputs.version }} + run: | + set -euo pipefail + helm dependency build deploy/operator + mkdir -p dist + helm package deploy/operator --destination dist + helm push "dist/operator-${VERSION}.tgz" "oci://${CHART_REPOSITORY}" diff --git a/.github/workflows/internal-image-publish.yaml b/.github/workflows/internal-image-publish.yaml index 8b15dc86..5cce00de 100644 --- a/.github/workflows/internal-image-publish.yaml +++ b/.github/workflows/internal-image-publish.yaml @@ -5,16 +5,50 @@ on: inputs: image_tag: type: string - description: 'Tags for Images in GAR' + description: 'Development tag in the form dev-<name>-<7-to-40-character-sha>' required: true jobs: release: - name: Release + name: Publish development image runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: + - name: Validate development tag + env: + VERSION: ${{ inputs.image_tag }} + run: | + if [[ ! "${VERSION}" =~ ^dev-[a-z0-9][a-z0-9._-]*-[0-9a-f]{7,40}$ ]]; then + echo "Development tags must use dev-<name>-<7-to-40-character-sha>" >&2 + exit 1 + fi + - name: Checkout - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: - persist-credentials: false fetch-depth: 0 + persist-credentials: false + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 + + - id: auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3 + with: + create_credentials_file: 'true' + token_format: access_token + project_id: wandb-production + workload_identity_provider: ${{ secrets.CI_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.CI_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }} + + - name: Authorize Docker for Artifact Registry + run: gcloud auth configure-docker us-docker.pkg.dev --quiet + + - name: Build and publish development image + env: + IMAGE_REPOSITORY: us-docker.pkg.dev/wandb-production/public/wandb/operator + VERSION: ${{ inputs.image_tag }} + run: make docker-build docker-push IMG="${IMAGE_REPOSITORY}:${VERSION}" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7e232d64..bec88a32 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,20 +1,25 @@ -name: Release v1 +name: Release v2 on: push: tags: - - "v1.*.*" + - "v2.*.*" permissions: contents: write + id-token: write concurrency: - group: release-v1 + group: release-v2 cancel-in-progress: false +env: + CHART_REPOSITORY: us-docker.pkg.dev/wandb-production/public/wandb/charts + IMAGE_REPOSITORY: us-docker.pkg.dev/wandb-production/public/wandb/operator + jobs: release: - name: Release v1 + name: Release v2 if: github.repository_owner == 'wandb' runs-on: ubuntu-latest steps: @@ -24,15 +29,15 @@ jobs: fetch-depth: 0 persist-credentials: false - - name: Validate release tag + - name: Validate tag and release versions id: release shell: bash run: | set -euo pipefail tag="${GITHUB_REF_NAME}" - if [[ ! "${tag}" =~ ^v(1)\.([0-9]+)\.([0-9]+)$ ]]; then - echo "Expected a stable v1 tag in the form v1.x.y; got ${tag}" >&2 + if [[ ! "${tag}" =~ ^v2\.([0-9]+)\.([0-9]+)$ ]]; then + echo "Expected a stable v2 tag in the form v2.x.y; got ${tag}" >&2 exit 1 fi @@ -41,72 +46,100 @@ jobs: exit 1 fi - git fetch --no-tags origin refs/heads/v1:refs/remotes/origin/v1 + git fetch --no-tags origin refs/heads/main:refs/remotes/origin/main tagged_commit="$(git rev-list -n 1 "${tag}")" - if ! git merge-base --is-ancestor "${tagged_commit}" refs/remotes/origin/v1; then - echo "Tagged commit ${tagged_commit} is not reachable from origin/v1" >&2 + if ! git merge-base --is-ancestor "${tagged_commit}" refs/remotes/origin/main; then + echo "Tagged commit ${tagged_commit} is not reachable from origin/main" >&2 exit 1 fi version="${tag#v}" - minor="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" - major="${BASH_REMATCH[1]}" + chart_version="$(awk '$1 == "version:" { print $2; exit }' deploy/operator/Chart.yaml | tr -d '\"')" + app_version="$(awk '$1 == "appVersion:" { print $2; exit }' deploy/operator/Chart.yaml | tr -d '\"')" + image_tag="$(awk ' + $0 == "wandb-operator:" { in_operator = 1; next } + in_operator && $0 == " image:" { in_image = 1; next } + in_image && $1 == "tag:" { print $2; exit } + ' deploy/operator/values.yaml | tr -d '\"')" + + for value in "${chart_version}" "${app_version}" "${image_tag}"; do + if [[ "${value}" != "${version}" ]]; then + echo "Chart version, appVersion, and operator image tag must all equal ${version}" >&2 + echo "Found chart=${chart_version}, appVersion=${app_version}, image=${image_tag}" >&2 + exit 1 + fi + done echo "tag=${tag}" >> "${GITHUB_OUTPUT}" echo "tagged_commit=${tagged_commit}" >> "${GITHUB_OUTPUT}" echo "version=${version}" >> "${GITHUB_OUTPUT}" - echo "minor=${minor}" >> "${GITHUB_OUTPUT}" - echo "major=${major}" >> "${GITHUB_OUTPUT}" - - name: Login to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + - name: Install Helm + uses: azure/setup-helm@bf6a7d304bc2fdb57e0331155b7ebf2c504acf0a # v4 with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} + version: v3.19.0 + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 - - name: Login to Quay.io - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + - id: auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3 with: - registry: quay.io - username: ${{ secrets.QUAY_USERNAME }} - password: ${{ secrets.QUAY_TOKEN }} + create_credentials_file: 'true' + token_format: access_token + project_id: wandb-production + workload_identity_provider: ${{ secrets.CI_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.CI_WORKLOAD_IDENTITY_SERVICE_ACCOUNT }} - - name: Build release image - env: - VERSION: ${{ steps.release.outputs.version }} - run: make docker-build IMG="wandb/controller:${VERSION}" + - name: Authorize Docker for Artifact Registry + run: gcloud auth configure-docker us-docker.pkg.dev --quiet - - name: Publish Docker Hub tags + - name: Reject existing production artifacts env: - MAJOR: ${{ steps.release.outputs.major }} - MINOR: ${{ steps.release.outputs.minor }} VERSION: ${{ steps.release.outputs.version }} + shell: bash run: | set -euo pipefail - docker push "wandb/controller:${VERSION}" - docker tag "wandb/controller:${VERSION}" "wandb/controller:${MINOR}" - docker push "wandb/controller:${MINOR}" - docker tag "wandb/controller:${VERSION}" "wandb/controller:${MAJOR}" - docker push "wandb/controller:${MAJOR}" - docker tag "wandb/controller:${VERSION}" wandb/controller:latest - docker push wandb/controller:latest + assert_absent() { + local artifact=$1 + local output + local status + + set +e + output="$(gcloud artifacts docker images describe "${artifact}" --format='value(image_summary.digest)' 2>&1)" + status=$? + set -e + + if [[ ${status} -eq 0 ]]; then + echo "Refusing to overwrite existing artifact ${artifact}" >&2 + exit 1 + fi + if ! grep -Eqi 'NOT_FOUND|not found' <<< "${output}"; then + echo "Could not safely determine whether ${artifact} exists:" >&2 + echo "${output}" >&2 + exit 1 + fi + } + + assert_absent "${IMAGE_REPOSITORY}:${VERSION}" + assert_absent "${CHART_REPOSITORY}/operator:${VERSION}" + + - name: Build and publish image + env: + VERSION: ${{ steps.release.outputs.version }} + run: make docker-build docker-push IMG="${IMAGE_REPOSITORY}:${VERSION}" - - name: Publish Quay.io tags + - name: Package and publish chart env: - MAJOR: ${{ steps.release.outputs.major }} - MINOR: ${{ steps.release.outputs.minor }} VERSION: ${{ steps.release.outputs.version }} run: | set -euo pipefail - - quay_image="quay.io/wandb_tools/wandb-k8s-operator" - docker tag "wandb/controller:${VERSION}" "${quay_image}:${VERSION}" - docker push "${quay_image}:${VERSION}" - docker tag "wandb/controller:${VERSION}" "${quay_image}:${MINOR}" - docker push "${quay_image}:${MINOR}" - docker tag "wandb/controller:${VERSION}" "${quay_image}:${MAJOR}" - docker push "${quay_image}:${MAJOR}" + helm dependency build deploy/operator + mkdir -p dist + helm package deploy/operator --destination dist + helm push "dist/operator-${VERSION}.tgz" "oci://${CHART_REPOSITORY}" - name: Create GitHub release env: diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml index 6f0bb815..0f6bd009 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/run-tests.yaml @@ -2,10 +2,12 @@ name: "Run Tests" on: push: branches: - - v1 + - v2 + - main pull_request: branches: - - v1 + - v2 + - main jobs: test: name: Test @@ -15,13 +17,29 @@ jobs: - name: Setup Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: - go-version: 1.25 + go-version: 1.26.3 - name: Install dependencies run: go mod download - name: Install Ginkgo - run: go install github.com/onsi/ginkgo/v2/ginkgo@latest + run: go install github.com/onsi/ginkgo/v2/ginkgo@v2.28.1 + - name: Install Helm + uses: azure/setup-helm@bf6a7d304bc2fdb57e0331155b7ebf2c504acf0a # v4 + with: + version: v3.19.0 + - name: Resolve chart dependencies + run: | + helm repo add ci-wandb https://charts.wandb.ai/ + helm repo add ci-moco https://cybozu-go.github.io/moco/ + helm repo add ci-ot-container-kit https://ot-container-kit.github.io/helm-charts + helm repo add ci-seaweedfs https://seaweedfs.github.io/seaweedfs-operator/ + helm repo add ci-prometheus-community https://prometheus-community.github.io/helm-charts + helm repo add ci-altinity https://helm.altinity.com + helm repo add ci-victoria-metrics https://victoriametrics.github.io/helm-charts/ + helm repo add ci-grafana https://grafana.github.io/helm-charts + helm dependency build deploy/operator + git diff --exit-code deploy/operator/Chart.lock - name: Tests - run: make test-coverage + run: make test build: name: Build @@ -31,7 +49,7 @@ jobs: - name: Setup Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: - go-version: 1.25 + go-version: 1.26.3 - name: Build run: make build @@ -43,7 +61,7 @@ jobs: - name: Setup Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: - go-version: 1.25 + go-version: 1.26.3 - name: Install dependencies run: go mod download - name: Check for changes in go.mod or go.sum diff --git a/.gitignore b/.gitignore index e920c762..f299d494 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ bin testbin/* Dockerfile.cross +.codex # Test binary, build with `go test -c` *.test @@ -30,11 +31,23 @@ junit.xml *.swo *~ +*.tgz + +/tmp /charts /cdk8s /git tilt-settings.json +tilt-settings.star +hack/testing-manifests/wandb/.generated/*.yaml /tilt_bin +/dist + +.DS_Store + +# Local webhook development certificates +.local-webhook-certs/ -.DS_Store \ No newline at end of file +# various util artifacts +.k8s-images diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..e3048b55 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,22 @@ +version: "2" +linters: + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ + - pkg/vendored/ +formatters: + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ + - pkg/vendored/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..b13b834d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,122 @@ +# CLAUDE.md + +Guidance for AI agents working in this repository. Read this before making changes. + +## What this is + +The **Weights & Biases Kubernetes operator** (`github.com/wandb/operator`) — a +Kubebuilder/controller-runtime operator that installs and reconciles a W&B +deployment plus its backing infrastructure (MySQL, Redis, ClickHouse, Kafka, +object storage) on a cluster. Built with Go and the Operator SDK; deployed via +Helm/Kustomize; developed locally with Kind + Tilt. + +## Architecture map + +| Path | What lives here | +|------|-----------------| +| `api/v1/`, `api/v2/` | CRD Go types (`*_types.go`). **`v2` is the storage/hub version; `v1` is a conversion spoke.** | +| `cmd/manager/` | Operator manager entrypoint (`main.go`). | +| `cmd/crd-installer/` | Standalone job that installs/updates CRDs (embeds CRDs from `internal/crdinstaller/crds`). | +| `internal/controller/` | Reconcilers. `weightsandbiases_controller.go` and `application_controller.go` are the top-level controllers. | +| `internal/controller/reconciler/` | The v2 reconcile logic, split by concern (`mysql.go`, `redis.go`, `clickhouse.go`, `kafka.go`, `objectstore.go`, `ingress.go`, `gateway.go`, `telemetry_*.go`, `reconcile_v2.go`, …). | +| `internal/controller/infra/managed/` vs `infra/external/` | Each backing service can be **managed** (operator provisions it) or **external** (user supplies a connection). This managed/external split is a core concept. | +| `internal/controller/common/` | Shared reconcile helpers — conditions, labels, retention, detach, state. | +| `internal/webhook/v2/` | Defaulting + validation + conversion webhooks for the CRDs. | +| `pkg/wandb/` | W&B server spec/manifest/status handling (manifest source resolution, channels). | +| `pkg/vendored/` | **Generated/vendored** third-party operator types & CRDs (Moco, Strimzi-Kafka, SeaweedFS, Altinity-ClickHouse, Argo-Rollouts, Redis, Gateway API). Do not hand-edit. | +| `config/` | Kustomize bases, CRD bases/patches, RBAC, manager config. | +| `hack/testing-manifests/` | Local-dev/test fixtures: sample `WeightsAndBiases` CRs (`wandb/`), a Helm chart for external infra (`test-infra/`), cert-manager/seaweedfs/telemetry manifests, and versioned local server manifests (`server-manifest/`). Used by Tilt and tests, not shipped. | +| `docs/design/wandb_v2/` | Design docs for the v2 architecture and infra reconciliation. | + +## The server manifest + +A **server manifest** is the W&B-published description of a given W&B server +version: which applications to run, their infra requirements (bucket, +clickhouse, kafka, mysql, redis), generated secrets, DB migrations, feature +flags, and shared env-var/volume groups (see `Manifest` in +[pkg/wandb/manifest/manifest.go](pkg/wandb/manifest/manifest.go)). + +- The CR points at one via `spec.wandb.manifestRepository` + `spec.wandb.version`. + The reconciler resolves it through `manifest.GetServerManifest(...)` — an OCI + artifact pulled with ORAS, or a `file://` path for local manifests. +- **It drives reconciliation.** [reconcile_v2.go](internal/controller/reconciler/reconcile_v2.go) + loads the manifest, then uses it to apply infra sizing, generate secrets, + create Kafka topics, run the MySQL init job and migrations, and reconcile each + application. The CRD describes *desired W&B/infra shape*; the manifest supplies + the *version-specific contents* the reconciler fills that shape with. +- **Published manifests are generated upstream** from + [wandb/core `onprem/server-manifest`](https://github.com/wandb/core/tree/master/onprem/server-manifest) + and published as OCI artifacts — they are **not** authored in this repo. +- For local work, `hack/testing-manifests/server-manifest/<version>/` holds + checked-in copies (e.g. `0.79.0`). Set `manifestSource="local"` in + `tilt-settings.star` (with a matching `wandbVersion`) to reconcile against them + instead of the published repository. + +## Critical workflow rules + +- **API types are the source of truth, but generated artifacts must be kept in + sync by hand.** After editing any `api/**/*_types.go`, run: + ```bash + make manifests generate sync-crd-embed + ``` + This regenerates deepcopy methods, the CRD YAML in `config/crd/bases/`, and the + embedded CRDs used by the crd-installer. CI fails if these are out of date. +- **Tilt does NOT regenerate CRDs automatically** when API types change — it only + recompiles the controller binary. See [DEVELOPMENT.md](DEVELOPMENT.md) for the + full "what to run when X changes" matrix. When in doubt, restart Tilt. +- **Don't edit generated files**: `zz_generated.deepcopy.go`, `config/crd/bases/*`, + `internal/crdinstaller/crds/*`, and anything under `pkg/vendored/`. Edit the + source and regenerate. +- **Counterfeiter fakes**: regenerate with `go generate ./...` after changing a + mocked interface. + +## Build / test / lint + +- `make test` — runs `manifests generate sync-crd-embed vet` + unit tests via + envtest (downloads binaries on first run); produces `coverage.html`. +- `make lint` — golangci-lint. `make lint-fix` to autofix. +- `make build` — regenerates, vets, and builds the manager + crd-installer binaries. +- **Always run `make lint` and `make test` before considering a task complete.** +- Tests use **Ginkgo/Gomega** (`suite_test.go` files set up envtest). CI runs + `make test-coverage` and `make build` on Go 1.25. + +## Commit & PR conventions + +- PR titles (and the squash commit) **must follow Conventional Commits** — a CI + check enforces it. Allowed types: `fix`, `feat`, `docs`, `style`, `refactor`, + `perf`, `test`, `build`, `ci`, `chore`, `revert`. +- The subject **must start with an uppercase letter** (e.g. + `feat: Add retention policy validation`). +- Releases are automated via **semantic-release** off `main` (`feat` → minor, + `fix` → patch, `!`/`BREAKING CHANGE` → major). `CHANGELOG.md` is generated — do + not edit it by hand. + +## Code style + +### Comments +- **Do NOT add inline comments that simply restate what the code does.** +- Only comment to explain WHY, not WHAT — business logic, non-obvious behavior, + important context. +- **Keep comments concise.** Prefer a short one-liner; only expand to multiple + lines when the context genuinely can't be conveyed briefly. + +```go +// Bad +// increment the counter +count++ + +// Good — explains a non-obvious constraint +// apiserver bounces v2 → v1 → v2 on admission; stash raw values so the round-trip is lossless +``` + +### Dependencies +- Scripts should explicitly check for required tools (e.g. `jq`) and fail with a + clear message if missing. Don't add fallbacks for missing dependencies unless + asked. + +## Where to learn more + +- [README.md](README.md) — local dev setup (Kind, Tilt, Kubebuilder, Kustomize). +- [DEVELOPMENT.md](DEVELOPMENT.md) — the regenerate-on-change flow and common issues. +- [docs/config-api.md](docs/config-api.md), [docs/infra-connection-settings.md](docs/infra-connection-settings.md), + [docs/monitoring.md](docs/monitoring.md) — CR config, external infra, telemetry. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 00000000..3dce8300 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,217 @@ +# Development Flow Guide + +This guide explains what to execute when different things change during development to ensure changes are properly deployed. + +## Understanding the Flow + +The operator has several interconnected components: +1. **API Types** (`api/v1/`, `api/v2/`) - Go structs defining CRDs +2. **Generated CRDs** (`config/crd/bases/`) - Kubernetes YAML generated from Go types +3. **Kustomize Patches** (`config/crd/patches/`) - Modifications to generated CRDs +4. **Controller Code** (`controllers/`) - Reconciliation logic +5. **Manager Binary** (`tilt_bin/manager`) - Compiled operator + +## Critical Issue: CRD Update Flow + +**The Problem**: Tilt does NOT automatically regenerate CRDs when API types change. Here's why: + +1. Line 76 of `Tiltfile` runs `local(manifests() + generate())` **once at startup** +2. Line 89 runs `local(manifests() + 'kustomize build config/default')` **once at startup** +3. Lines 109-110: The `Watch&Compile` local resource only watches for code changes and recompiles the binary +4. **CRDs are never regenerated when API types change during development** + +## What to Execute When Things Change + +### 1. API Types Changed (`api/v1/*_types.go` or `api/v2/*_types.go`) + +**What needs to happen**: +- Regenerate Go deepcopy methods +- Regenerate CRD YAML files +- Rebuild and apply CRDs to cluster +- Recompile controller binary + +**Manual Steps** (current workaround): +```bash +# Regenerate everything +make manifests generate + +# Let Tilt detect the change and rebuild the binary +# OR manually trigger: tilt trigger Watch&Compile + +# Apply updated CRDs manually +kubectl apply -f config/crd/bases/apps.wandb.com_weightsandbiases.yaml +kubectl apply -f config/crd/bases/apps.wandb.com_applications.yaml +``` + +**Better Approach** (restart Tilt): +```bash +# Stop Tilt (Ctrl+C) +# Restart Tilt +tilt up +``` + +### 2. Controller Logic Changed (`controllers/*.go`, `pkg/*.go`) + +**What needs to happen**: +- Recompile controller binary +- Restart controller pod + +**Steps**: +```bash +# Tilt automatically handles this via Watch&Compile local resource +# The live_update will sync the new binary and restart the container +# No manual action needed - just save the file +``` + +### 3. CRD Patches Changed (`config/crd/patches/*.yaml`) + +**What needs to happen**: +- Rebuild kustomize output +- Apply updated CRDs to cluster + +**Steps**: +```bash +# Stop Tilt (Ctrl+C) +# Restart Tilt to regenerate kustomize output +tilt up + +# OR manually apply +make manifests +kubectl apply -f <(kustomize build config/crd) +``` + +### 4. Test CR Changed (`hack/testing-manifests/wandb/*.yaml`) + +**What needs to happen**: +- Reapply the custom resource + +**Steps**: +```bash +# Tilt automatically watches this file (via watch_settings) +# Just save the file and Tilt will reapply it +# No manual action needed +``` + +### 5. Kustomize Config Changed (`config/default/*`, `config/manager/*`) + +**What needs to happen**: +- Rebuild kustomize output +- Reapply all resources + +**Steps**: +```bash +# Stop Tilt (Ctrl+C) +# Restart Tilt +tilt up +``` + +## Recommended Tiltfile Improvements + +The current Tiltfile has these limitations: + +1. **CRDs not watched**: Changes to `api/` require manual `make manifests` or Tilt restart +2. **Kustomize not watched**: Changes to kustomize configs require Tilt restart +3. **One-time generation**: `local()` commands only run at Tilt startup + +### Proposed Fix + +Add a local resource to watch and regenerate CRDs: + +```python +# Add this after line 110 in Tiltfile +local_resource('Watch&Regenerate-CRDs', + manifests(), + deps=['api'], + ignore=['*/*/zz_generated.deepcopy.go'], + auto_init=False, # Don't run automatically, only on trigger + trigger_mode=TRIGGER_MODE_MANUAL +) +``` + +Then manually trigger when API types change: +```bash +tilt trigger Watch&Regenerate-CRDs +``` + +### Alternative: Auto-regenerate CRDs + +```python +# Replace line 76 with a local_resource +local_resource('Generate-CRDs', + manifests() + generate(), + deps=['api'], + ignore=['*/*/zz_generated.deepcopy.go', 'config/crd/bases/*.yaml'], + auto_init=True +) + +# Note: This may cause Tilt to restart frequently during active API development +``` + +## Quick Reference + +| What Changed | Automatic? | Manual Steps | +|-------------|-----------|--------------| +| `api/**/*_types.go` | ❌ No | `make manifests generate`, then restart Tilt or manually apply CRDs | +| `controllers/**/*.go` | ✅ Yes | Tilt auto-rebuilds and restarts | +| `pkg/**/*.go` | ✅ Yes | Tilt auto-rebuilds and restarts | +| `config/crd/patches/*.yaml` | ❌ No | Restart Tilt | +| `hack/testing-manifests/wandb/*.yaml` | ✅ Yes | Tilt auto-applies | +| `config/default/**/*.yaml` | ❌ No | Restart Tilt | + +## Testing Changes + +Always test before committing: + +```bash +# Run linter +make lint + +# Run unit tests +make test + +# View coverage +open coverage.html + +# Test in cluster (Tilt should be running) +# 1. Make changes +# 2. Wait for Tilt to rebuild (or trigger manually) +# 3. Check logs in Tilt UI +# 4. Verify CR status: kubectl get wandb -o yaml +``` + +## Common Issues + +### Issue: "CRD changes not applied" +**Cause**: Tilt doesn't regenerate CRDs automatically +**Fix**: Run `make manifests generate` and restart Tilt, or manually apply CRDs + +### Issue: "Controller crashes with 'field not found' error" +**Cause**: CRD not updated but controller code expects new fields +**Fix**: Ensure CRDs are regenerated and applied before controller restarts + +### Issue: "kubectl shows old CRD version" +**Cause**: CRD not reapplied to cluster +**Fix**: `kubectl apply -f config/crd/bases/apps.wandb.com_*.yaml` + +### Issue: "Changes to v2 API not taking effect" +**Cause**: Kustomize patch may be disabling v2 or `wandbCR` setting wrong +**Fix**: Check `config/crd/patches/use_v2.yaml` and `tilt-settings.star` + +## Full Clean Rebuild + +When in doubt: + +```bash +# Stop Tilt +# Ctrl+C in Tilt terminal + +# Clean everything +make clean # if this target exists +rm -rf bin/ tilt_bin/ + +# Regenerate +make manifests generate + +# Rebuild and redeploy +tilt up +``` \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 67f0d3bc..554e717b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.25 AS manager-builder +FROM golang:1.26 AS manager-builder ARG TARGETOS ARG TARGETARCH @@ -13,7 +13,7 @@ COPY go.sum go.sum RUN go mod download # Copy the go source -COPY cmd/main.go cmd/main.go +COPY cmd/ cmd/ COPY api/ api/ COPY pkg/ pkg/ COPY internal/ internal/ @@ -23,22 +23,23 @@ COPY internal/ internal/ # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager ./cmd/manager +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o crd-installer ./cmd/crd-installer FROM registry.access.redhat.com/ubi9/ubi-minimal + WORKDIR / COPY --from=manager-builder /workspace/manager . +COPY --from=manager-builder /workspace/crd-installer . -# Create a helm cache directory and set ownership to the non-root user -RUN mkdir -p /helm/.cache/helm /helm/.config/helm /helm/.local/share/helm && chown -R 65532:65532 /helm +RUN mkdir -p /helm/.cache/helm /helm/.config/helm /helm/.local/share/helm && \ + chown -R 65532:65532 /helm USER 65532:65532 ENV HELM_CACHE_HOME=/helm/.cache/helm ENV HELM_CONFIG_HOME=/helm/.config/helm ENV HELM_DATA_HOME=/helm/.local/share/helm - ENV OPERATOR_MODE=production -ENV DEPLOYER_API_URL=https://deploy.wandb.ai/api ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile index c3633e82..b66628d1 100644 --- a/Makefile +++ b/Makefile @@ -43,11 +43,34 @@ help: ## Display this help. .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. - $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) rbac:roleName=manager-role crd:allowDangerousTypes=true,generateEmbeddedObjectMeta=true,maxDescLen=0 webhook paths="{./api/v1,./api/v2,./internal/controller/...,./internal/webhook/...}" output:crd:artifacts:config=config/crd/bases .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. - $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="{./api/v1,./api/v2}" + +.PHONY: generate-vendored +generate-vendored: ## Regenerate vendored Moco CRDs from the operator's Helm chart dependency. + @helm dependency update deploy/operator >/dev/null + @tar -xzOf deploy/operator/charts/moco-*.tgz moco/templates/generated/crds/moco_crds.yaml | \ + sed -e '/^{{/d' \ + -e "s|: '{{ .Release.Service }}'|: Helm|g" \ + -e "s|: '{{ include \"moco.name\" . }}'|: moco|g" \ + -e "s|: '{{ .Chart.AppVersion }}'|: vendored|g" \ + -e "s|: '{{ include \"moco.chart\" . }}'|: moco-vendored|g" \ + -e "s|: '{{ .Release.Namespace }}/moco-serving-cert'|: moco-system/moco-serving-cert|g" \ + -e "s|: '{{ .Release.Namespace }}'|: moco-system|g" \ + > pkg/vendored/moco/crds/moco_crds.yaml + @echo "Regenerated pkg/vendored/moco/crds/moco_crds.yaml" + +.PHONY: sync-crd-embed +sync-crd-embed: manifests ## Sync embedded CRDs in internal/crdinstaller/crds from their source-of-truth locations. + @mkdir -p internal/crdinstaller/crds/operator internal/crdinstaller/crds/redis internal/crdinstaller/crds/clickhouse + @rm -f internal/crdinstaller/crds/operator/*.yaml internal/crdinstaller/crds/redis/*.yaml internal/crdinstaller/crds/clickhouse/*.yaml + @cp config/crd/bases/apps.wandb.com_*.yaml internal/crdinstaller/crds/operator/ + @cp pkg/vendored/redis-operator/crds/*.yaml internal/crdinstaller/crds/redis/ + @cp pkg/vendored/altinity-clickhouse/crds/clickhouse.altinity.com_clickhouseinstallations.yaml internal/crdinstaller/crds/clickhouse/ + @echo "Synced CRDs into internal/crdinstaller/crds/{operator,redis,clickhouse}/" .PHONY: fmt fmt: ## Run go fmt against code. @@ -58,14 +81,39 @@ vet: ## Run go vet against code. go vet ./... .PHONY: test -test: manifests generate fmt vet setup-envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out +test: manifests generate sync-crd-embed vet setup-envtest ## Run tests. + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test -v $$(go list ./... | grep -v /e2e | grep -v /vendored) -coverprofile cover.out + +.PHONY: setup-local-webhook +setup-local-webhook: ## Setup local webhook development environment with certificates. + @./scripts/setup-local-webhook.sh + +.PHONY: run-local-webhook +run-local-webhook: manifests generate fmt vet ## Run controller locally with webhook support. + @if [ ! -d ".local-webhook-certs" ]; then \ + echo "Webhook certificates not found. Run 'make setup-local-webhook' first."; \ + exit 1; \ + fi + go run ./cmd/manager \ + --webhook-cert-path=.local-webhook-certs \ + --webhook-cert-name=tls.crt \ + --webhook-cert-key=tls.key \ + --v2-webhook=true # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. # Prometheus and CertManager are installed by default; skip with: # - PROMETHEUS_INSTALL_SKIP=true # - CERT_MANAGER_INSTALL_SKIP=true +.PHONY: test-e2e-retention +test-e2e-retention: manifests generate fmt vet ## Run retention policy integration tests against a real cluster. + @command -v kubectl >/dev/null 2>&1 || { \ + echo "kubectl is not installed."; \ + exit 1; \ + } + PROMETHEUS_INSTALL_SKIP=true CERT_MANAGER_INSTALL_SKIP=true USE_EXISTING_CLUSTER=true \ + go test ./test/e2e/ -v -ginkgo.v -ginkgo.focus="Retention Policy" -timeout 90m + .PHONY: test-e2e test-e2e: manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. @command -v kind >/dev/null 2>&1 || { \ @@ -143,23 +191,30 @@ local-registry-stop: ## Stop and remove the local OCI registry. ##@ Build .PHONY: build -build: manifests generate fmt vet ## Build manager binary. - go build -o bin/manager cmd/main.go +build: manifests generate fmt vet sync-crd-embed build-manager build-crd-installer ## Build all binaries. + +.PHONY: build-manager +build-manager: ## Build the manager binary. + go build -o bin/manager ./cmd/manager + +.PHONY: build-crd-installer +build-crd-installer: ## Build the crd-installer binary. + go build -o bin/crd-installer ./cmd/crd-installer .PHONY: run -run: manifests generate fmt vet ## Run a controller from your host. - go run ./cmd/main.go +run: manifests generate fmt vet ## Run the manager from your host. + go run ./cmd/manager # If you wish to build the manager image targeting other platforms you can use the --platform flag. # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ .PHONY: docker-build -docker-build: ## Build docker image with the manager. - $(CONTAINER_TOOL) build -t ${IMG} . +docker-build: ## Build controller docker image. + $(CONTAINER_TOOL) build --platform linux/amd64 -t ${IMG} -f Dockerfile . .PHONY: docker-push -docker-push: ## Push docker image with the manager. - $(CONTAINER_TOOL) push ${IMG} +docker-push: + $(CONTAINER_TOOL) push --platform linux/amd64 ${IMG} # PLATFORMS defines the target platforms for the manager image be built to provide support to multiple # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: @@ -178,6 +233,15 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform - $(CONTAINER_TOOL) buildx rm operator-builder rm Dockerfile.cross +.PHONY: docker-buildx-push +docker-buildx-push: + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile.controller > Dockerfile.cross + - $(CONTAINER_TOOL) buildx create --name controller-builder + $(CONTAINER_TOOL) buildx use controller-builder + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - $(CONTAINER_TOOL) buildx rm controller-builder + rm Dockerfile.cross + .PHONY: build-installer build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. mkdir -p dist @@ -192,7 +256,11 @@ endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - --server-side + +.PHONY: apply-local-dev +apply-local-dev: manifests kustomize ## Apply local webhook development configuration (CRDs, RBAC, webhook). + $(KUSTOMIZE) build config/local-dev | $(KUBECTL) apply -f - --server-side .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. @@ -223,12 +291,12 @@ GOLANGCI_LINT = $(LOCALBIN)/golangci-lint ## Tool Versions KUSTOMIZE_VERSION ?= v5.5.0 -CONTROLLER_TOOLS_VERSION ?= v0.17.1 +CONTROLLER_TOOLS_VERSION ?= v0.19.0 #ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') -GOLANGCI_LINT_VERSION ?= v1.63.4 +GOLANGCI_LINT_VERSION ?= v2.12.2 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. @@ -256,7 +324,7 @@ $(ENVTEST): $(LOCALBIN) .PHONY: golangci-lint golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) - $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary @@ -280,4 +348,4 @@ $(GINKGO): $(LOCALBIN) test -s $(LOCALBIN)/ginkgo || GOBIN=$(LOCALBIN) go install github.com/onsi/ginkgo/v2/ginkgo@latest -include dep-management.mk olm.mk ginko.mk \ No newline at end of file +include dep-management.mk olm.mk diff --git a/PROJECT b/PROJECT index 145bcae0..a801132e 100644 --- a/PROJECT +++ b/PROJECT @@ -21,4 +21,32 @@ resources: kind: WeightsAndBiases path: github.com/wandb/operator/api/v1 version: v1 +- api: + crdVersion: v1 + namespaced: true + domain: wandb.com + group: apps + kind: WeightsAndBiases + path: github.com/wandb/operator/api/v2 + version: v2 + webhooks: + conversion: true + defaulting: true + spoke: + - v1 + validation: true + webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: wandb.com + group: apps + kind: Application + path: github.com/wandb/operator/api/v2 + version: v2 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 version: "3" diff --git a/README.md b/README.md index 9102ea31..eedb9385 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,90 @@ -# operator +# W&B Operator -// TODO(user): Add simple overview of use/purpose +A Kubernetes operator for deploying and managing self-hosted [Weights & Biases](https://wandb.ai) +on your own cluster. + +This branch contains Operator v2. Maintenance and release history for the +legacy Operator v1 line are available on the [`v1` branch](https://github.com/wandb/operator/tree/v1). ## Description -// TODO(user): An in-depth paragraph about your project and overview of use +The operator turns a single `WeightsAndBiases` custom resource into a fully running +W&B platform. You declare the desired state — version, scale, networking, and which +backing services to use — and the operator reconciles the application together with +its dependencies (database, cache, message queue, object storage, and analytics +store) to match. + +It supports two modes for backing infrastructure: + +- **Managed**: the operator provisions and operates dependencies in-cluster through + bundled component operators: + 1. MySQL via [Moco](https://github.com/cybozu-go/moco) + 2. Redis + 3. Kafka via [Strimzi](https://strimzi.io/) + 4. Object Storage via + [SeaweedFS](https://github.com/seaweedfs/seaweedfs) + 5. [ClickHouse](https://github.com/Altinity/clickhouse-operator) +- **External**: you point the resource at infrastructure you already run (for + example a managed cloud database or object store) and the operator connects to it + instead of provisioning its own. + +An optional telemetry stack ([VictoriaMetrics](https://victoriametrics.com/) and +[Grafana](https://grafana.com/)) can be enabled to collect metrics and ship +pre-built dashboards for the deployment. + +### Custom Resources + +| Kind | Group/Version | Purpose | +| --- | --- | --- | +| `WeightsAndBiases` | `apps.wandb.com/v2` | Top-level desired state for a W&B deployment and its backing services. | +| `Application` | `apps.wandb.com/v2` | Lower-level building block the operator uses to render a workload (Deployment, Service, Ingress/HTTPRoute, autoscaling, jobs) for a component. | + +`v1` of `WeightsAndBiases` is still served; a conversion webhook converts between +`v1` and `v2`. + +## Installation + +The operator and its component dependencies are distributed as a Helm chart +([`deploy/operator`](deploy/operator)), published as an OCI artifact. + +```bash +helm install wandb-operator \ + oci://us-docker.pkg.dev/wandb-production/public/wandb/charts/operator \ + --namespace wandb-operators --create-namespace +``` + +Then apply a `WeightsAndBiases` resource describing your deployment: + +```yaml +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb + namespace: wandb +spec: + size: small + retentionPolicy: + onDelete: detach + wandb: + version: <wandb-version> + networking: + mode: ingress +``` + +```bash +kubectl apply -f wandb.yaml +``` + +The operator reconciles the resource, brings up the requested backing services, +and rolls out the W&B application. See [`deploy/operator/values.yaml`](deploy/operator/values.yaml) +for the available chart options and which component operators are enabled. + +## Documentation + +- [Configuration API](docs/config-api.md) +- [Infrastructure Connection Settings](docs/infra-connection-settings.md) +- [Monitoring and Telemetry Guide](docs/monitoring.md) +- [Deploying on OpenShift](docs/openshift.md) ## Development @@ -12,25 +92,55 @@ - A Kubernetes cluster (e.g. [kind](https://kind.sigs.k8s.io/)) - [Tilt](https://tilt.dev/) +- [Kubebuilder](https://book.kubebuilder.io/quick-start.html) +- [Kustomize](https://kustomize.io/) +- [jq](https://stedolan.github.io/jq/) for some helper scripts +- [helm](https://github.com/helm/helm) + +### Install Chart Testing + +In order to lint and test your helm chart for creating a release, please install helm's `ct` command: + +```bash +brew install chart-testing +``` + +If any new chart has been updated, or installed, you can run the following command to ensure everything is good! + +```bash +ct lint --config deploy/ct.yaml +``` #### Install Kind -A kubernetes cluster is required to run the operator. -[kind](https://kind.sigs.k8s.io/) is recommended for local development. Install -`kind` and create a cluster: +A kubernetes cluster is required to run the operator. [kind](https://kind.sigs.k8s.io/) is recommended for local development. Install `kind` and +create a cluster: ```bash brew install kind +``` + +#### Create a Cluster + +```bash kind create cluster ``` -This will create a new kind cluster with the name `kind`. The kubernetes context -will be called `kind-kind`. +This will create a new kind cluster with the name `kind`. The kubernetes context will be called `kind-kind`. + +Alternatively, you can use the provided scripts to manage the kind cluster. + +```bash +# Create cluster +./hack/scripts/setup_kind.sh + +# Delete cluster +./hack/scripts/teardown_kind.sh +``` #### Install Tilt -[Tilt](https://tilt.dev/) is a tool for local development of Kubernetes -applications. Install `tilt`: +[Tilt](https://tilt.dev/) is a tool for local development of Kubernetes applications. Install `tilt`: ```bash brew install tilt @@ -38,32 +148,70 @@ brew install tilt #### Install Kubebuilder -[kubebuilder](https://book.kubebuilder.io/quick-start.html) is a tool for -building Kubernetes operators. Install `kubebuilder`: +[kubebuilder](https://book.kubebuilder.io/quick-start.html) is a tool for building Kubernetes operators. Install `kubebuilder`: ```bash brew install kubebuilder ``` +#### Install Kustomize + +[kustomize](https://kustomize.io/) is used to build Kubernetes manifests. Install `kustomize`: + +```bash +brew install kustomize +``` + ### Configuring and Running Tilt #### Tilt Settings -There are settings for Tilt that can be configured using a `tilt-settings.json` -file. The settings file is not checked into source control. A sample settings -file is provided in `tilt-settings.sample.json`. To use the sample settings -file, copy it to `tilt-settings.json` +Tilt reads local settings from `tilt-settings.star`. The file is not checked +into source control; start from `tilt-settings.sample.star` and keep local +overrides there. + +The default Tilt setup follows the normal operator install path: + +- installs one `wandb-operator` Helm release in `wandb-operators` +- builds the local controller image as `controller:latest` +- creates a `WeightsAndBiases` CR in `wandb` +- uses `networkMode="gateway"` with `http://localhost:8080` +- uses the published server manifest repository by default +- keeps telemetry off unless `observabilityMode="full"` is set + +Common W&B CR settings are scalar values such as `wandbHostname`, +`wandbVersion`, `size`, `retentionPolicy`, `licenseFile`, `manifestSource`, +and `networkMode`. +Set `networkMode="ingress"` to use the local ingress-nginx path instead of +Gateway API; if `wandbHostname` is not set explicitly, ingress mode uses +`http://wandb.localhost:8080`. -By default, Tilt is configured to only allow connections to the following -Kubernetes contexts: +Tilt defaults `manifestSource="published"`, which leaves +`spec.wandb.manifestRepository` empty so the W&B CR webhook applies the same +published OCI repository default as production installs. To test repo-local server manifest +definitions, set `manifestSource="local"` and keep +`localManifestPath="hack/testing-manifests/server-manifest"`. The default local +manifest path currently contains `0.79.0`, so also set `wandbVersion="0.79.0"` +when using that local source. + +Use `crFile` for custom CR shapes; Tilt treats it as a base CR and still +applies the scalar settings above. + +By default, Tilt is configured to only allow connections to the following Kubernetes contexts: - `docker-desktop` - `kind-kind` +- `kind-wandb-operator` - `minikube` - `orbstack` +- `crc-admin` -Please add any additional contexts to the `allowedContexts` list in your -`tilt-settings.json` file. +Please add any additional contexts to the `allowedContexts` list in your `tilt-settings.star` file. + +For CRC/OpenShift Local, run `./hack/scripts/setup_crc.sh` and use the +`crc-admin` context. Tilt auto-enables `openshiftSCC` for CRC, pushes the dev +image through CRC's internal registry, and applies the OpenShift Helm profile. +For other OpenShift clusters, set `openshiftSCC=True` explicitly. #### Running Tilt @@ -71,8 +219,45 @@ Please add any additional contexts to the `allowedContexts` list in your tilt up ``` +#### Cleaning Up Tilt + +`tilt down` removes Tilt-managed workloads and Helm releases, but it intentionally does not +fully reset the cluster. The following are expected to survive a normal `tilt down`: + +- `cert-manager` and its namespace +- `kube-state-metrics` and its namespace +- operator CRDs, including the W&B CRDs and operator dependency CRDs +- `wandb-operators` and dependency namespaces +- dev PVC-backed data unless the backing operator deletes it + +For a true dev reset, use the helper script instead: + +```bash +./hack/scripts/tilt-down-dev-clean.sh +``` + +This performs a safer teardown sequence for local development: + +1. Deletes the `WeightsAndBiases` CR +2. Waits for finalizer-driven cleanup while the operators are still running +3. Uninstalls the Tilt-managed Helm releases +4. Deletes dev PVCs and generated secrets for the app +5. Runs `tilt down` + +If you are already in the Tilt UI, you can trigger the manual `Dev-Clean` resource first, +then run `tilt down`. + ## Testing +### Locally testing external infra + +1. Install the WandB CR with Tilt using the default `retentionPolicy="detach"` in `tilt-settings.star`. +2. Delete the WandB CR — infra should be detached but remain in place. +3. Run `./hack/scripts/managed-connections-to-external.sh` to convert the managed connection secrets into external ones. +4. Install the WandB CR with Tilt using a custom `crFile` that points at a CR + with the external infra connection specs. +5. WandB should now run with externally managed infra. + ### Counterfeiter ```bash diff --git a/Tiltfile b/Tiltfile index 8db8086e..a22ebac6 100644 --- a/Tiltfile +++ b/Tiltfile @@ -1,128 +1,938 @@ -# default values +# Local operator development. +# +# Tilt keeps the fast local controller loop while installing the operator +# through the same Helm chart path as a normal install. + +GENERATED_DIR = "hack/testing-manifests/wandb/.generated" +GENERATED_WANDB_CR = GENERATED_DIR + "/tilt-wandb-cr.yaml" +GENERATED_OPERATOR_VALUES = GENERATED_DIR + "/tilt-operator-values.yaml" +GENERATED_CUSTOM_CA_CONFIGMAP = GENERATED_DIR + "/tilt-custom-ca-configmap.yaml" + +GATEWAY_API_CRDS_URL = "https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml" +IMG = "controller:latest" + +GROUP_DEPENDENCIES = "Dependencies" +GROUP_WANDB_APP = "Wandb-App" +GROUP_TELEMETRY = "Telemetry" +GROUP_WANDB_OPERATOR = "Wandb-Operator" + settings = { "allowedContexts": [ "docker-desktop", "minikube", "kind-kind", + "kind-wandb-operator", "orbstack", + "crc-admin", ], - "installMinio": True, - "installWandb": True, - "wandbCRD": "default", + + # Operator install settings. + "operatorNamespace": "wandb-operators", + "openshiftSCC": False, + + # W&B CR settings. + "includeCR": True, + "crFile": "", + "wandbName": "wandb", + "wandbNamespace": "wandb", + "wandbHostname": "http://localhost:8080", + "wandbVersion": "0.83.0-clickhouse-keeper.2", + "size": "dev", + "retentionPolicy": "detach", + "licenseFile": "", + "manifestSource": "published", # published or local + "localManifestPath": "hack/testing-manifests/server-manifest", + "networkMode": "gateway", # gateway or ingress + "gatewayClass": "nginx", + "ingressClass": "nginx", + "createCA": True, + "issuerName": "", + + # off, full, forward. + "observabilityMode": "off", + + "logFormat": "pretty", # pretty, text, json + + "useExternalMysql": False, + "useExternalRedis": False, + "useExternalObjectStore": False, + "useCustomCA": False, } -# global settings -settings.update(read_json( - "tilt-settings.json", - default={}, -)) +if os.path.exists("tilt-settings.json"): + fail("tilt-settings.json is no longer supported. Migrate to tilt-settings.star (see tilt-settings.sample.star).") + +if not os.path.exists("tilt-settings.star"): + local("cp tilt-settings.sample.star tilt-settings.star") + +load("./tilt-settings.star", "SETTINGS") +settings.update(SETTINGS) + + +def warn(message): + print("WARNING: " + message) + + +def as_bool(value): + if value == True: + return True + if value == False or value == None: + return False + return str(value).lower() in ["true", "yes", "1", "on"] + + +def bool_string(value): + if value: + return "true" + return "false" + + +def normalize_observability_mode(): + mode = str(settings.get("observabilityMode", "off")).lower() + if mode in ["off", "full", "forward"]: + return mode + + fail("observabilityMode must be one of: off, full, forward") + + +def normalize_network_mode(): + mode = str(settings.get("networkMode", "gateway")).lower() + if mode in ["gateway", "ingress"]: + return mode + + fail("networkMode must be one of: gateway, ingress") + + +def normalize_manifest_source(): + source = str(settings.get("manifestSource", "published")).lower() + if source in ["published", "local"]: + return source + + fail("manifestSource must be one of: published, local") + + +def shell_quote(value): + return "'" + str(value).replace("'", "'\"'\"'") + "'" + + +def write_file_cmd(path, contents): + return "cat > %s <<'EOF'\n%s\nEOF" % (path, contents) + + +def k8s_yaml_object(obj): + k8s_yaml(encode_yaml(obj)) + + +def repo_path(path): + path = str(path) + if path.startswith("./"): + return path + return "./" + path -# Configure global watch settings with a 2-second debounce -watch_settings(ignore=["**/.git", "**/*.out"]) -if k8s_context() in settings.get("allowedContexts"): +def validate_local_manifest_path(path): + path = str(path) + if path.startswith("/") or path.startswith("../") or "/../" in path or path == "..": + fail("localManifestPath must be a repo-relative Docker build-context path.") + if " " in path: + fail("localManifestPath cannot contain spaces because it is used in a Dockerfile ADD instruction.") + if not os.path.exists(path): + fail("manifestSource='local' requires localManifestPath to exist: %s" % path) + return path + + +def write_generated_yaml(path, obj): + local("mkdir -p " + GENERATED_DIR) + local(write_file_cmd(path, encode_yaml(obj))) + return path + + +def helm_supports_take_ownership(): + return str(local("helm upgrade --help | grep -q -- '--take-ownership' && echo true || echo false")).strip() == "true" + + +def url_host(url): + rest = str(url) + if "://" in rest: + rest = rest.split("://", 1)[1] + host_port = rest.split("/", 1)[0] + if "@" in host_port: + host_port = host_port.split("@", 1)[1] + parts = host_port.split(":") + return parts[0] + + +def url_port(url): + rest = str(url) + if "://" in rest: + rest = rest.split("://", 1)[1] + host_port = rest.split("/", 1)[0] + parts = host_port.split(":") + if len(parts) > 1: + return int(parts[len(parts) - 1]) + if str(url).startswith("https://"): + return 443 + return 80 + + +settings["networkMode"] = normalize_network_mode() +settings["observabilityMode"] = normalize_observability_mode() +settings["manifestSource"] = normalize_manifest_source() +settings["openshiftSCC"] = as_bool(settings.get("openshiftSCC")) +if settings["manifestSource"] == "local": + settings["localManifestPath"] = validate_local_manifest_path(settings.get("localManifestPath")) + +USE_EXTERNAL_MYSQL = as_bool(settings.get("useExternalMysql")) +USE_EXTERNAL_REDIS = as_bool(settings.get("useExternalRedis")) +USE_EXTERNAL_OBJECT_STORE = as_bool(settings.get("useExternalObjectStore")) +USE_CUSTOM_CA = as_bool(settings.get("useCustomCA")) +USE_EXTERNAL_INFRA = USE_EXTERNAL_MYSQL or USE_EXTERNAL_REDIS or USE_EXTERNAL_OBJECT_STORE +USE_TEST_INFRA_TLS = USE_CUSTOM_CA and (USE_EXTERNAL_MYSQL or USE_EXTERNAL_REDIS) + +if (USE_EXTERNAL_INFRA or USE_CUSTOM_CA) and not as_bool(settings.get("includeCR")): + fail("useExternalMysql/useExternalRedis/useExternalObjectStore/useCustomCA require includeCR=True") +if (USE_EXTERNAL_INFRA or USE_CUSTOM_CA) and settings.get("wandbCR", "") != "": + fail("useExternalMysql/useExternalRedis/useExternalObjectStore/useCustomCA patch generated CRs; use crFile instead of wandbCR") + +watch_settings(ignore=["**/.git", "**/*.out", GENERATED_DIR + "/**"]) +update_settings(k8s_upsert_timeout_secs=300) + +currentContext = k8s_context() +if currentContext in settings.get("allowedContexts"): print("Context is allowed") else: fail("Selected context is not in allow list") -allow_k8s_contexts(settings.get("allowed_k8s_contexts")) +allow_k8s_contexts(settings.get("allowedContexts")) + +IS_CRC = "crc" in currentContext or "api-crc-testing" in currentContext +if IS_CRC: + settings["openshiftSCC"] = True + default_registry( + "default-route-openshift-image-registry.apps-crc.testing/%s" % settings.get("operatorNamespace"), + host_from_cluster="image-registry.openshift-image-registry.svc:5000/%s" % settings.get("operatorNamespace"), + ) + +os.putenv("PATH", "./bin:" + os.getenv("PATH")) -os.putenv('PATH', './bin:' + os.getenv('PATH')) +load("ext://helm_resource", "helm_repo", "helm_resource") -load('ext://restart_process', 'docker_build_with_restart') +def operator_dockerfile(): + lines = [ + "FROM registry.access.redhat.com/ubi9/ubi", + "", + "ADD tilt_bin/manager /manager", + "ADD tilt_bin/crd-installer /crd-installer", + ] -DOCKERFILE = ''' -FROM registry.access.redhat.com/ubi9/ubi-minimal + if settings.get("manifestSource") == "local": + lines.append("ADD %s /server-manifest" % settings.get("localManifestPath")) -ADD tilt_bin/manager /manager + lines.extend([ + "", + "RUN mkdir -p /helm/.cache/helm /helm/.config/helm /helm/.local/share/helm && chown -R 65532:65532 /helm", + "USER 65532:65532", + "ENV HELM_CACHE_HOME=/helm/.cache/helm", + "ENV HELM_CONFIG_HOME=/helm/.config/helm", + "ENV HELM_DATA_HOME=/helm/.local/share/helm", + "", + ]) -RUN mkdir -p /helm/.cache/helm /helm/.config/helm /helm/.local/share/helm && chown -R 65532:65532 /helm + return "\n".join(lines) -USER 65532:65532 -ENV HELM_CACHE_HOME=/helm/.cache/helm -ENV HELM_CONFIG_HOME=/helm/.config/helm -ENV HELM_DATA_HOME=/helm/.local/share/helm -''' -DOMAIN = "wandb.com" -GROUP = "apps" -VERSION = "v1" -KIND = "wandb" -IMG = 'controller:latest' -CONTROLLERGEN = 'rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases;' -DISABLE_SECURITY_CONTEXT = True +def binary(): + return "CGO_ENABLED=0 GOOS=linux GO111MODULE=on go build -o tilt_bin/manager ./cmd/manager && CGO_ENABLED=0 GOOS=linux GO111MODULE=on go build -o tilt_bin/crd-installer ./cmd/crd-installer" -def manifests(): - return 'controller-gen ' + CONTROLLERGEN +def managed_endpoint_resource(name, anchor_object, deps, local_port, remote_port, link_name, pod_selector, labels, local_host="localhost"): + k8s_resource( + new_name=name, + objects=[anchor_object], + discovery_strategy="selectors-only", + extra_pod_selectors=[pod_selector], + resource_deps=deps, + port_forwards=[ + port_forward(local_port, remote_port, name=link_name, host=local_host), + ], + labels=labels, + ) -def generate(): - return 'controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./...";' +def endpoint_anchor(name): + return { + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": name, + "namespace": "default", + }, + "data": { + "managed-by": "tilt", + }, + } -def vetfmt(): - return 'go vet ./...; go fmt ./...' -# build to tilt_bin because kubebuilder has a dockerignore for bin/ +def build_endpoint_anchors(names): + for name in names: + k8s_yaml_object(endpoint_anchor(name)) + + +def build_wandb_namespace(namespace): + k8s_yaml_object({ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": { + "name": namespace, + "labels": { + "app.kubernetes.io/managed-by": "tilt", + }, + }, + }) + + +def build_operator_values(telemetry_namespace): + telemetry_enabled = settings.get("observabilityMode") != "off" + grafana_enabled = settings.get("observabilityMode") == "full" + + values = { + "wandb": { + "install": False, + }, + "wandb-operator": { + "image": { + "pullPolicy": "IfNotPresent", + }, + "containers": { + "operator": { + "command": [], + }, + }, + }, + "victoria-metrics-operator": { + "enabled": telemetry_enabled, + "crds": { + "plain": True, + }, + "admissionWebhooks": { + "enabled": False, + }, + }, + "grafana-operator": { + "enabled": grafana_enabled, + }, + "telemetry": { + "mode": settings.get("observabilityMode"), + "namespace": telemetry_namespace, + }, + } + + manager_entrypoint = ["/manager", "--log-format=" + settings.get("logFormat")] + + values["wandb-operator"]["containers"]["operator"]["command"] = manager_entrypoint + + if settings.get("openshiftSCC"): + values["wandb-operator"]["podSecurityContext"] = { + "runAsUser": None, + "runAsGroup": None, + "fsGroup": None, + "fsGroupChangePolicy": None, + } + values["wandb-operator"]["containers"]["operator"]["env"] = { + "OPENSHIFT": { + "value": "true", + }, + } + values["altinity-clickhouse-operator"] = { + "crdHook": { + "enabled": False, + }, + } + + return write_generated_yaml(GENERATED_OPERATOR_VALUES, values) + + +def helper_flag(name, value): + if value == None or value == "": + return "" + return " --%s %s" % (name, shell_quote(value)) -def binary(): - return 'CGO_ENABLED=0 GOOS=linux GO111MODULE=on go build -o tilt_bin/manager cmd/main.go' +def helper_bool_flag(name, value): + return " --%s=%s" % (name, bool_string(as_bool(value))) -installed = local("which kubebuilder") -print("kubebuilder is present:", installed) -DIRNAME = os.path.basename(os. getcwd()) +def build_wandb_cr(): + if settings.get("wandbCR"): + return settings.get("wandbCR") + else: + cmd = "go run ./hack/tilt/wandbcr" + cmd += helper_flag("out", GENERATED_WANDB_CR) + cmd += helper_flag("cr-file", settings.get("crFile", "")) + cmd += helper_flag("name", settings.get("wandbName")) + cmd += helper_flag("namespace", settings.get("wandbNamespace")) + cmd += helper_flag("hostname", settings.get("wandbHostname")) + cmd += helper_flag("version", settings.get("wandbVersion")) + cmd += helper_flag("size", settings.get("size")) + cmd += helper_flag("retention-policy", settings.get("retentionPolicy")) + cmd += helper_flag("license-file", settings.get("licenseFile", "")) + cmd += helper_flag("manifest-source", settings.get("manifestSource")) + cmd += helper_flag("observability-mode", settings.get("observabilityMode")) + cmd += helper_flag("network-mode", settings.get("networkMode")) + cmd += helper_flag("gateway-class", settings.get("gatewayClass")) + cmd += helper_flag("ingress-class", settings.get("ingressClass")) + cmd += helper_bool_flag("create-ca", settings.get("createCA")) + cmd += helper_flag("issuer-name", settings.get("issuerName", "")) + cmd += helper_bool_flag("external-mysql", USE_EXTERNAL_MYSQL) + cmd += helper_bool_flag("external-redis", USE_EXTERNAL_REDIS) + cmd += helper_bool_flag("external-objectstore", USE_EXTERNAL_OBJECT_STORE) + cmd += helper_bool_flag("custom-ca", USE_CUSTOM_CA) + if USE_CUSTOM_CA: + cmd += helper_flag("custom-ca-configmap-out", GENERATED_CUSTOM_CA_CONFIGMAP) + local(cmd) -local(manifests() + generate()) + return GENERATED_WANDB_CR -if settings.get("installMinio"): - k8s_yaml('./hack/testing-manifests/minio/minio.yaml') + +def build_wandb_ca(name, namespace): + root_cert_name = name + "-root-cert" + selfsigned_issuer_name = name + "-selfsigned-issuer" + ca_issuer_name = name + "-ca-issuer" + + k8s_yaml_object({ + "apiVersion": "cert-manager.io/v1", + "kind": "Issuer", + "metadata": { + "name": selfsigned_issuer_name, + "namespace": namespace, + }, + "spec": { + "selfSigned": {}, + }, + }) + k8s_yaml_object({ + "apiVersion": "cert-manager.io/v1", + "kind": "Certificate", + "metadata": { + "name": root_cert_name, + "namespace": namespace, + }, + "spec": { + "secretName": root_cert_name, + "isCA": True, + "commonName": "wandb-ca", + "duration": "210240h", + "issuerRef": { + "name": selfsigned_issuer_name, + "kind": "Issuer", + "group": "cert-manager.io", + }, + }, + }) + k8s_yaml_object({ + "apiVersion": "cert-manager.io/v1", + "kind": "Issuer", + "metadata": { + "name": ca_issuer_name, + "namespace": namespace, + }, + "spec": { + "ca": { + "secretName": root_cert_name, + }, + }, + }) k8s_resource( - 'minio', - 'Minio', + new_name="WandB-CA", objects=[ - 'minio:service', - 'minio:namespace' + "%s:issuer:%s" % (selfsigned_issuer_name, namespace), + "%s:certificate:%s" % (root_cert_name, namespace), + "%s:issuer:%s" % (ca_issuer_name, namespace), + ], + resource_deps=["cert-manager"], + labels=[GROUP_WANDB_APP], + ) + + +WANDB_CR = build_wandb_cr() if as_bool(settings.get("includeCR")) else "" +WANDB_CR_CONTENT = read_yaml(WANDB_CR) if as_bool(settings.get("includeCR")) else {} +WANDB_NAME = WANDB_CR_CONTENT.get("metadata", {}).get("name", settings.get("wandbName")) +WANDB_NAMESPACE = WANDB_CR_CONTENT.get("metadata", {}).get("namespace", settings.get("wandbNamespace")) +OPERATOR_VALUES = build_operator_values(WANDB_NAMESPACE) +CREATE_WANDB_NAMESPACE = as_bool(settings.get("includeCR")) or settings.get("observabilityMode") != "off" + +if WANDB_CR_CONTENT.get("apiVersion") == "apps.wandb.com/v1": + WANDB_HOSTNAME = WANDB_CR_CONTENT.get("spec", {}).get("values", {}).get("global", {}).get("host", settings.get("wandbHostname")) + ingress_create = WANDB_CR_CONTENT.get("spec", {}).get("values", {}).get("ingress", {}).get("create", True) + ingress_install = WANDB_CR_CONTENT.get("spec", {}).get("values", {}).get("ingress", {}).get("install", True) + LOCAL_NETWORKING_MODE = "ingress" if ingress_install and ingress_create else settings.get("networkMode") +else: + WANDB_HOSTNAME = WANDB_CR_CONTENT.get("spec", {}).get("wandb", {}).get("hostname", settings.get("wandbHostname")) + LOCAL_NETWORKING_MODE = WANDB_CR_CONTENT.get("spec", {}).get("networking", {}).get("mode", settings.get("networkMode")) + +endpoint_anchors = [] +if as_bool(settings.get("includeCR")): + if LOCAL_NETWORKING_MODE in ["gateway", "ingress"]: + endpoint_anchors.append("wandb-endpoint-anchor") + +if settings.get("observabilityMode") == "full": + endpoint_anchors += [ + "telemetry-grafana-endpoint-anchor", + "telemetry-victoria-metrics-endpoint-anchor", + "telemetry-victoria-logs-endpoint-anchor", + "telemetry-victoria-traces-endpoint-anchor", + ] + +if endpoint_anchors: + build_endpoint_anchors(endpoint_anchors) + +if CREATE_WANDB_NAMESPACE: + build_wandb_namespace(WANDB_NAMESPACE) + k8s_resource( + new_name="WandB-Namespace", + objects=["%s:namespace" % WANDB_NAMESPACE], + labels=[GROUP_DEPENDENCIES], + ) + +if USE_CUSTOM_CA: + k8s_yaml(GENERATED_CUSTOM_CA_CONFIGMAP) + k8s_resource( + new_name="Custom-CA-ConfigMap", + objects=["wandb-user-ca-certs:configmap:%s" % WANDB_NAMESPACE], + resource_deps=["WandB-Namespace"], + labels=[GROUP_WANDB_APP], + ) + +local_resource( + "Operator-Codegen", + "make manifests generate", + labels=[GROUP_WANDB_OPERATOR], +) + +local_resource( + "Operator-Build", + binary(), + deps=["internal", "pkg", "api", "cmd"], + resource_deps=["Operator-Codegen"], + ignore=["*/*/zz_generated.deepcopy.go"], + labels=[GROUP_WANDB_OPERATOR], +) + +local_resource( + "Operator-Chart-Deps", + "helm dependency build ./deploy/operator --skip-refresh", + deps=[ + "deploy/operator/Chart.yaml", + "deploy/operator/Chart.lock", + "deploy/telemetry/Chart.yaml", + "deploy/telemetry/values.yaml", + "deploy/telemetry/templates", + "deploy/telemetry/dashboards", + ], + labels=[GROUP_DEPENDENCIES], +) + +# local_resource( +# "WandB-CRDs-Apply", +# "kubectl apply --server-side=true --force-conflicts --field-manager=helm " + +# "-f config/crd/bases/apps.wandb.com_applications.yaml " + +# "-f config/crd/bases/apps.wandb.com_weightsandbiases.yaml", +# resource_deps=["Operator-Codegen"], +# labels=[GROUP_DEPENDENCIES], +# ) +# +# local_resource( +# "WandB-CRDs-Ready", +# "kubectl wait --for=condition=established --timeout=120s " + +# "crd/applications.apps.wandb.com " + +# "crd/weightsandbiases.apps.wandb.com", +# resource_deps=["WandB-CRDs-Apply"], +# labels=[GROUP_DEPENDENCIES], +# ) + +cert_manager_flags = [ + "--create-namespace", + "--version=v1.20.2", + "--set=crds.enabled=true", + "--set=startupapicheck.enabled=false", +] +cert_manager_deps = [] + +if LOCAL_NETWORKING_MODE == "gateway": + if settings.get("openshiftSCC"): + # OpenShift Ingress Operator owns Gateway API CRDs via a + # ValidatingAdmissionPolicy and blocks kubectl apply. Just assert + # the CRDs the wandb chart actually uses are present. + gateway_crd_check = ( + "kubectl get crd " + + "gatewayclasses.gateway.networking.k8s.io " + + "gateways.gateway.networking.k8s.io " + + "httproutes.gateway.networking.k8s.io " + + "referencegrants.gateway.networking.k8s.io " + + "grpcroutes.gateway.networking.k8s.io" + ) + local_resource( + "gateway-api-crds", + gateway_crd_check, + labels=[GROUP_DEPENDENCIES], + ) + else: + local_resource( + "gateway-api-crds", + "kubectl apply -f " + GATEWAY_API_CRDS_URL, + labels=[GROUP_DEPENDENCIES], + ) + cert_manager_flags.append("--set=config.enableGatewayAPI=true") + cert_manager_deps.append("gateway-api-crds") + +helm_resource( + "cert-manager", + chart="oci://quay.io/jetstack/charts/cert-manager", + namespace="cert-manager", + flags=cert_manager_flags, + resource_deps=cert_manager_deps, + labels=[GROUP_DEPENDENCIES], +) + +if USE_EXTERNAL_INFRA: + test_infra_deps = ["WandB-Namespace"] + if USE_TEST_INFRA_TLS: + test_infra_deps.append("cert-manager") + + helm_resource( + "Test-Infra", + chart="./hack/testing-manifests/test-infra", + release_name="test-infra", + namespace=WANDB_NAMESPACE, + flags=[ + "--create-namespace", + "--wait", + "--timeout=10m", + "--set=mysql.enabled=%s" % bool_string(USE_EXTERNAL_MYSQL), + "--set=redis.enabled=%s" % bool_string(USE_EXTERNAL_REDIS), + "--set=seaweedfs.enabled=%s" % bool_string(USE_EXTERNAL_OBJECT_STORE), + "--set=tls.enabled=%s" % bool_string(USE_TEST_INFRA_TLS), + "--set=mysql.tls.enabled=%s" % bool_string(USE_CUSTOM_CA and USE_EXTERNAL_MYSQL), + "--set=redis.tls.enabled=%s" % bool_string(USE_CUSTOM_CA and USE_EXTERNAL_REDIS), + ], + deps=["hack/testing-manifests/test-infra/"], + resource_deps=test_infra_deps, + labels=[GROUP_WANDB_APP], + ) + +if LOCAL_NETWORKING_MODE == "gateway": + nginx_gateway_flags = [ + "--create-namespace", + "--version=2.5.1", + ] + if currentContext.startswith("kind-"): + nginx_gateway_flags += [ + "--set=nginx.service.type=NodePort", + "--set=nginx.service.nodePorts[0].port=31437", + "--set=nginx.service.nodePorts[0].listenerPort=8080", + "--set=nginx.service.nodePorts[1].port=30478", + "--set=nginx.service.nodePorts[1].listenerPort=8443", ] + + helm_resource( + "nginx-gateway-fabric", + chart="oci://ghcr.io/nginx/charts/nginx-gateway-fabric", + namespace="nginx-gateway", + flags=nginx_gateway_flags, + resource_deps=["gateway-api-crds"], + labels=[GROUP_DEPENDENCIES], ) -k8s_yaml(local(manifests() + 'kustomize build config/default')) - -k8s_resource( - new_name='CRD', - objects=['weightsandbiases.apps.wandb.com:customresourcedefinition']) -k8s_resource( - new_name='RBAC', - objects=[ - 'operator-manager-role:clusterrole', - 'operator-manager-rolebinding:clusterrolebinding', - 'operator-leader-election-role:role', - 'operator-leader-election-rolebinding:rolebinding' +if LOCAL_NETWORKING_MODE == "ingress": + helm_repo( + "ingress-nginx", + "https://kubernetes.github.io/ingress-nginx", + resource_name="ingress-nginx-repo", + labels=[GROUP_DEPENDENCIES], + ) + helm_resource( + "ingress-nginx-controller", + chart="ingress-nginx/ingress-nginx", + release_name="ingress-nginx", + namespace="ingress-nginx", + flags=[ + "--create-namespace", + "--version=4.14.1", + "--set-string=controller.ingressClass=%s" % settings.get("ingressClass"), + "--set-string=controller.ingressClassResource.name=%s" % settings.get("ingressClass"), + "--set-string=controller.service.type=ClusterIP", + ], + resource_deps=["ingress-nginx-repo"], + labels=[GROUP_DEPENDENCIES], + ) + +kube_state_metrics_flags = [ + "--create-namespace", + "--version=5.27.0", +] +if settings.get("openshiftSCC"): + # Null the chart's hardcoded 65534 IDs so restricted-v2 assigns valid ones. + kube_state_metrics_flags += [ + "--set=securityContext.runAsUser=null", + "--set=securityContext.runAsGroup=null", + "--set=securityContext.fsGroup=null", ] + +helm_resource( + "kube-state-metrics", + chart="oci://ghcr.io/prometheus-community/charts/kube-state-metrics", + namespace="kube-state-metrics", + flags=kube_state_metrics_flags, + labels=[GROUP_DEPENDENCIES], +) + +operator_deps = ["Operator-Chart-Deps", "Operator-Build"] +operator_deps.append("cert-manager") +operator_deps.append("kube-state-metrics") +if LOCAL_NETWORKING_MODE == "gateway": + operator_deps.append("nginx-gateway-fabric") +if settings.get("observabilityMode") != "off": + operator_deps.append("WandB-Namespace") + +operator_flags = ["--create-namespace"] +if helm_supports_take_ownership(): + operator_flags.append("--take-ownership") +else: + warn("helm does not support --take-ownership; legacy CRD ownership may require Dev-Clean before the operator release can install.") + +operator_flags += [ + "-f", + OPERATOR_VALUES, +] +operator_deps_files = [ + OPERATOR_VALUES, + "deploy/operator/", +] + +if settings.get("openshiftSCC"): + operator_flags += [ + "--values=./deploy/operator/profiles/openshift.yaml", + ] + operator_deps_files.append("deploy/operator/profiles/openshift.yaml") + +helm_resource( + "wandb-operator", + chart="./deploy/operator", + release_name="wandb-operator", + namespace=settings.get("operatorNamespace"), + flags=operator_flags, + image_deps=[IMG], + image_keys=[("wandb-operator.image.repository", "wandb-operator.image.tag")], + deps=operator_deps_files, + resource_deps=operator_deps, + labels=[GROUP_WANDB_OPERATOR], +) + +local_resource( + "Operator-Webhook-Ready", + cmd="kubectl wait --for=condition=available --timeout=300s -n %s deploy/wandb-operator && " % settings.get("operatorNamespace") + + "until kubectl get mutatingwebhookconfiguration wandb-operator-mutating-webhook-configuration " + + "-o jsonpath='{.webhooks[0].clientConfig.caBundle}' | grep -q .; " + + "do echo 'Waiting for webhook CA bundle to be injected...'; sleep 2; done && echo 'Webhook is ready!'", + resource_deps=["wandb-operator"], + labels=[GROUP_WANDB_OPERATOR], +) + +local_resource( + "Dev-Clean", + "./hack/scripts/tilt-dev-clean.sh", + auto_init=False, + labels=[GROUP_WANDB_APP], ) -deps = ['controllers', 'pkg', 'cmd/main.go'] -deps.append('api') +if as_bool(settings.get("includeCR")): + wandb_deps = ["Operator-Webhook-Ready", "WandB-Namespace"] + if LOCAL_NETWORKING_MODE == "gateway": + wandb_deps.append("nginx-gateway-fabric") + if LOCAL_NETWORKING_MODE == "ingress": + wandb_deps.append("ingress-nginx-controller") + if USE_EXTERNAL_INFRA: + wandb_deps.append("Test-Infra") + if USE_CUSTOM_CA: + wandb_deps.append("Custom-CA-ConfigMap") -local_resource('Watch&Compile', generate() + binary(), - deps=deps, ignore=['*/*/zz_generated.deepcopy.go']) + if str(WANDB_HOSTNAME).startswith("https://") and as_bool(settings.get("createCA")): + build_wandb_ca(WANDB_NAME, WANDB_NAMESPACE) + wandb_deps.append("WandB-CA") + + k8s_yaml(WANDB_CR) -if settings.get("installWandb"): - k8s_yaml('./hack/testing-manifests/wandb/' + settings.get('wandbCRD') + '.yaml') k8s_resource( - new_name='Wandb', - objects=[ - 'wandb-default:weightsandbiases' - ], - resource_deps=["operator-controller-manager"] + new_name="Wandb", + objects=["%s:weightsandbiases:%s" % (WANDB_NAME, WANDB_NAMESPACE)], + resource_deps=wandb_deps, + labels=[GROUP_WANDB_APP], ) -docker_build_with_restart(IMG, '.', - dockerfile_contents=DOCKERFILE, - entrypoint='/manager', - only=['./tilt_bin/manager'], - live_update=[ - sync('./tilt_bin/manager', '/manager'), - ] - ) + if settings.get("openshiftSCC"): + # Dev-only frontend-nginx SCC; not shipped (prod uses its own ingress). + # It needs its fixed image UID: restricted-v2 assigns an arbitrary UID + # and nonroot-v2 rejects named user, so clone restricted-v2 + RunAsAny. + frontend_scc_name = "wandb-frontend-anyuid-v2" + k8s_yaml_object({ + "apiVersion": "security.openshift.io/v1", + "kind": "SecurityContextConstraints", + "metadata": { + "name": frontend_scc_name, + "labels": { + "app.kubernetes.io/managed-by": "tilt", + }, + }, + "allowHostDirVolumePlugin": False, + "allowHostIPC": False, + "allowHostNetwork": False, + "allowHostPID": False, + "allowHostPorts": False, + "allowPrivilegeEscalation": False, + "allowPrivilegedContainer": False, + "allowedCapabilities": ["NET_BIND_SERVICE"], + "readOnlyRootFilesystem": False, + "requiredDropCapabilities": ["ALL"], + "runAsUser": {"type": "RunAsAny"}, + "seLinuxContext": {"type": "MustRunAs"}, + "seccompProfiles": ["runtime/default"], + "fsGroup": {"type": "MustRunAs"}, + "supplementalGroups": {"type": "RunAsAny"}, + "volumes": [ + "configMap", + "csi", + "downwardAPI", + "emptyDir", + "ephemeral", + "image", + "persistentVolumeClaim", + "projected", + "secret", + ], + "users": [ + "system:serviceaccount:%s:wandb-app" % WANDB_NAMESPACE, + ], + }) + k8s_resource( + new_name="OpenShift-Frontend-SCC", + objects=["%s:securitycontextconstraints" % frontend_scc_name], + resource_deps=["WandB-Namespace"], + labels=[GROUP_WANDB_APP], + ) + # Stamp required-scc on frontend Deployment; reconcile merge keeps it. + local_resource( + "OpenShift-Frontend-SCC-Bind", + cmd=( + "until kubectl -n %s get deploy/frontend >/dev/null 2>&1; do " % WANDB_NAMESPACE + + "echo 'waiting for frontend deployment...'; sleep 3; done && " + + "kubectl -n %s patch deploy/frontend --type=merge -p " % WANDB_NAMESPACE + + shell_quote('{"spec":{"template":{"metadata":{"annotations":{"openshift.io/required-scc":"%s"}}}}}' % frontend_scc_name) + ), + resource_deps=["Wandb", "OpenShift-Frontend-SCC"], + labels=[GROUP_WANDB_APP], + ) + + endpoint_port = url_port(WANDB_HOSTNAME) + endpoint_host = url_host(WANDB_HOSTNAME) + + if LOCAL_NETWORKING_MODE == "gateway": + managed_endpoint_resource( + name="Wandb-Endpoint", + anchor_object="wandb-endpoint-anchor:configmap:default", + deps=["Wandb", "nginx-gateway-fabric"], + local_port=endpoint_port, + remote_port=endpoint_port, + link_name="W&B gateway", + local_host=endpoint_host, + pod_selector={ + "app.kubernetes.io/instance": "nginx-gateway-fabric", + "app.kubernetes.io/managed-by": "nginx-gateway-fabric-nginx", + "gateway.networking.k8s.io/gateway-name": "wandb-gateway", + }, + labels=[GROUP_WANDB_APP], + ) + elif LOCAL_NETWORKING_MODE == "ingress": + managed_endpoint_resource( + name="Wandb-Endpoint", + anchor_object="wandb-endpoint-anchor:configmap:default", + deps=["Wandb", "ingress-nginx-controller"], + local_port=endpoint_port, + remote_port=80, + link_name="W&B ingress", + local_host=endpoint_host, + pod_selector={ + "app.kubernetes.io/component": "controller", + "app.kubernetes.io/instance": "ingress-nginx", + "app.kubernetes.io/name": "ingress-nginx", + }, + labels=[GROUP_WANDB_APP], + ) + +if settings.get("observabilityMode") == "full": + managed_endpoint_resource( + name="Telemetry-Endpoint-Grafana", + anchor_object="telemetry-grafana-endpoint-anchor:configmap:default", + deps=["wandb-operator"], + local_port=3000, + remote_port=3000, + link_name="Grafana", + pod_selector={"app": "grafana"}, + labels=[GROUP_TELEMETRY], + ) + managed_endpoint_resource( + name="Telemetry-Endpoint-VictoriaMetrics", + anchor_object="telemetry-victoria-metrics-endpoint-anchor:configmap:default", + deps=["wandb-operator"], + local_port=8428, + remote_port=8429, + link_name="VictoriaMetrics UI", + pod_selector={ + "app.kubernetes.io/name": "vmsingle", + "app.kubernetes.io/instance": "victoria-instance", + }, + labels=[GROUP_TELEMETRY], + ) + managed_endpoint_resource( + name="Telemetry-Endpoint-VictoriaLogs", + anchor_object="telemetry-victoria-logs-endpoint-anchor:configmap:default", + deps=["wandb-operator"], + local_port=9428, + remote_port=9428, + link_name="VictoriaLogs", + pod_selector={ + "app.kubernetes.io/name": "vlsingle", + "app.kubernetes.io/instance": "victoria-logs", + }, + labels=[GROUP_TELEMETRY], + ) + managed_endpoint_resource( + name="Telemetry-Endpoint-VictoriaTraces", + anchor_object="telemetry-victoria-traces-endpoint-anchor:configmap:default", + deps=["wandb-operator"], + local_port=10428, + remote_port=10428, + link_name="VictoriaTraces", + pod_selector={ + "app.kubernetes.io/name": "vtsingle", + "app.kubernetes.io/instance": "victoria-traces", + }, + labels=[GROUP_TELEMETRY], + ) + +docker_only = ["./tilt_bin/manager", "./tilt_bin/crd-installer"] + +if settings.get("manifestSource") == "local": + path = repo_path(settings.get("localManifestPath")) + if not path.endswith(".yaml"): + paths = listdir(path, True) + for path in paths: + docker_only.append(path) + else: + docker_only.append(path) + +docker_build( + IMG, + ".", + dockerfile_contents=operator_dockerfile(), + only=docker_only, +) diff --git a/Tiltfile.chart b/Tiltfile.chart new file mode 100644 index 00000000..bb1fe9bd --- /dev/null +++ b/Tiltfile.chart @@ -0,0 +1,42 @@ +load('ext://namespace', 'namespace_create') +load('ext://cert_manager', 'deploy_cert_manager') + +deploy_cert_manager() +# default values +settings = { + "allowedContexts": [ + "docker-desktop", + "minikube", + "kind-kind", + "orbstack", + ], +} + +if os.path.exists("tilt-settings.json"): + fail("tilt-settings.json is no longer supported. Migrate to tilt-settings.star (see tilt-settings.sample.star).") + +if not os.path.exists("tilt-settings.star"): + local("cp tilt-settings.sample.star tilt-settings.star") + +load("./tilt-settings.star", "SETTINGS") +settings.update(SETTINGS) + +# Configure global watch settings with a 2-second debounce +watch_settings(ignore=["**/.git", "**/*.out"]) + +# Increase timeout for helm installations and apply operations +update_settings(k8s_upsert_timeout_secs=300) + +currentContext = k8s_context() + +if currentContext in settings.get("allowedContexts"): + print("Context is allowed") +else: + fail("Selected context is not in allow list") + +allow_k8s_contexts(settings.get("allowed_k8s_contexts")) + +current_namespace = k8s_namespace() +namespace_create(current_namespace) + +k8s_yaml(helm('./deploy/operator', 'wandb-operator', namespace=current_namespace, values=['./deploy/operator/values.yaml'])) diff --git a/api/v1/weightsandbiases_conversion.go b/api/v1/weightsandbiases_conversion.go new file mode 100644 index 00000000..f1b1061d --- /dev/null +++ b/api/v1/weightsandbiases_conversion.go @@ -0,0 +1,160 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "encoding/json" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + ctrl "sigs.k8s.io/controller-runtime" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/conversion" + + appsv2 "github.com/wandb/operator/api/v2" +) + +var logger = ctrl.Log.WithName("weightsandbiases-conversion") + +// Round-trip annotations stashed on v2 so ConvertFrom can reproduce the +// original v1 chart/values across apiserver-internal v2 → v1 → v2 bounces. +const ( + v1ChartAnnotation = "legacy.operator.wandb.com/v1-chart" + v1ValuesAnnotation = "legacy.operator.wandb.com/v1-values" +) + +const conversionLookupTimeout = 5 * time.Second + +var conversionReader ctrlclient.Reader + +// SetConversionReader wires a Reader (typically mgr.GetAPIReader()) into the +// conversion webhook for auxiliary lookups. Call before the manager starts. +func SetConversionReader(r ctrlclient.Reader) { + conversionReader = r +} + +// lookupSecret returns (nil, nil) when the reader isn't wired, name is +// empty, or the Secret doesn't exist; other errors propagate. +func lookupSecret(namespace, name string) (*corev1.Secret, error) { + if conversionReader == nil || name == "" { + return nil, nil + } + ctx, cancel := context.WithTimeout(context.Background(), conversionLookupTimeout) + defer cancel() + + var secret corev1.Secret + if err := conversionReader.Get(ctx, ctrlclient.ObjectKey{Namespace: namespace, Name: name}, &secret); err != nil { + if apierrors.IsNotFound(err) { + return nil, nil + } + return nil, fmt.Errorf("conversion read secret %s/%s: %w", namespace, name, err) + } + return &secret, nil +} + +// resolveValues prefers `<cr-name>-spec-active`'s data.values (the coalesced +// values written by the v1 reconciler) and falls back to src.Spec.Values +// when the Secret is absent, has no `values` key, or the reader isn't wired. +func resolveValues(src *WeightsAndBiases) (map[string]interface{}, error) { + secretName := fmt.Sprintf("%s-spec-active", src.Name) + secret, err := lookupSecret(src.Namespace, secretName) + if err != nil { + return nil, err + } + if secret == nil { + return src.Spec.Values.Object, nil + } + raw, ok := secret.Data["values"] + if !ok || len(raw) == 0 { + return src.Spec.Values.Object, nil + } + var values map[string]interface{} + if err := json.Unmarshal(raw, &values); err != nil { + return nil, fmt.Errorf("decode values from secret %s/%s: %w", src.Namespace, secretName, err) + } + return values, nil +} + +// ConvertTo converts this WeightsAndBiases (v1) to the Hub version (v2). +func (src *WeightsAndBiases) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*appsv2.WeightsAndBiases) + logger.Info("ConvertTo: Converting WeightsAndBiases from Spoke version v1 to Hub version v2", + "source", src.Namespace+"/"+src.Name, + ) + + dst.ObjectMeta = src.ObjectMeta + + if err := applyValueMappings(src, dst); err != nil { + return err + } + + // Stash raw v1 chart/values for round-trip recovery in ConvertFrom. + return stashV1Source(src, dst) +} + +// ConvertFrom restores the raw v1 chart/values from the annotations stashed +// by ConvertTo so apiserver's v2 → v1 → v2 admission bounces are lossless. +func (dst *WeightsAndBiases) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*appsv2.WeightsAndBiases) + logger.Info("ConvertFrom: Converting WeightsAndBiases from Hub version v2 to Spoke version v1", + "source", src.Namespace+"/"+src.Name, + ) + + dst.ObjectMeta = src.ObjectMeta + + if err := loadV1Source(src, dst); err != nil { + return err + } + return nil +} + +func stashV1Source(src *WeightsAndBiases, dst *appsv2.WeightsAndBiases) error { + chartJSON, err := json.Marshal(src.Spec.Chart.Object) + if err != nil { + return fmt.Errorf("marshal v1 chart: %w", err) + } + valuesJSON, err := json.Marshal(src.Spec.Values.Object) + if err != nil { + return fmt.Errorf("marshal v1 values: %w", err) + } + if dst.Annotations == nil { + dst.Annotations = make(map[string]string) + } + dst.Annotations[v1ChartAnnotation] = string(chartJSON) + dst.Annotations[v1ValuesAnnotation] = string(valuesJSON) + return nil +} + +func loadV1Source(src *appsv2.WeightsAndBiases, dst *WeightsAndBiases) error { + dst.Spec.Chart = Object{Object: map[string]interface{}{}} + dst.Spec.Values = Object{Object: map[string]interface{}{}} + + if raw, ok := src.Annotations[v1ChartAnnotation]; ok && raw != "" { + if err := json.Unmarshal([]byte(raw), &dst.Spec.Chart.Object); err != nil { + return fmt.Errorf("unmarshal %s: %w", v1ChartAnnotation, err) + } + } + if raw, ok := src.Annotations[v1ValuesAnnotation]; ok && raw != "" { + if err := json.Unmarshal([]byte(raw), &dst.Spec.Values.Object); err != nil { + return fmt.Errorf("unmarshal %s: %w", v1ValuesAnnotation, err) + } + } + return nil +} diff --git a/api/v1/weightsandbiases_conversion_mapping.go b/api/v1/weightsandbiases_conversion_mapping.go new file mode 100644 index 00000000..db9e64dd --- /dev/null +++ b/api/v1/weightsandbiases_conversion_mapping.go @@ -0,0 +1,740 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "encoding/json" + "fmt" + "sort" + "strconv" + "strings" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/utils/ptr" + + appsv2 "github.com/wandb/operator/api/v2" +) + +// Default Secret keys used by v1's legacy ref blocks when only the Secret +// name was specified. +const ( + defaultMySQLPasswordSecretKey = "MYSQL_PASSWORD" + defaultRedisPasswordSecretKey = "REDIS_PASSWORD" + defaultOIDCClientSecretKey = "OIDC_SECRET" + defaultBucketAccessKeyName = "ACCESS_KEY" + defaultBucketSecretKeyName = "SECRET_KEY" +) + +// Annotations carrying v1 literals the reconciler materializes into Secrets +// post-conversion (the webhook is stateless and can't create them itself). +const ( + OIDCPendingAnnotation = "legacy.operator.wandb.com/oidc-pending" + MySQLPendingAnnotation = "legacy.operator.wandb.com/mysql-pending" + RedisPendingAnnotation = "legacy.operator.wandb.com/redis-pending" + BucketPendingAnnotation = "legacy.operator.wandb.com/bucket-pending" +) + +var validSizes = map[string]appsv2.Size{ + string(appsv2.SizeDev): appsv2.SizeDev, + string(appsv2.SizeMicro): appsv2.SizeMicro, + string(appsv2.SizeSmall): appsv2.SizeSmall, + string(appsv2.SizeMedium): appsv2.SizeMedium, + string(appsv2.SizeLarge): appsv2.SizeLarge, + string(appsv2.SizeXLarge): appsv2.SizeXLarge, + string(appsv2.SizeXXLarge): appsv2.SizeXXLarge, +} + +// applyValueMappings is the top-level conversion orchestrator. It resolves +// the authoritative values (active-spec Secret if present, else the CR), +// runs peer-of-global mappers inline, and delegates global.* to +// applyGlobalMappings. +func applyValueMappings(src *WeightsAndBiases, dst *appsv2.WeightsAndBiases) error { + values, err := resolveValues(src) + if err != nil { + return err + } + if values == nil { + return nil + } + + if err := mapVersion(values, dst); err != nil { + return err + } + if err := mapServiceAccountAnnotations(values, dst); err != nil { + return err + } + if err := mapInternalJWTIssuer(values, dst); err != nil { + return err + } + if err := mapIngress(values, dst); err != nil { + return err + } + if err := mapLegacyOverrides(values, dst); err != nil { + return err + } + + globalMap, found, err := unstructured.NestedMap(values, "global") + if err != nil { + return fmt.Errorf("spec.values.global: %w", err) + } + if found { + if err := applyGlobalMappings(globalMap, dst); err != nil { + return err + } + } + + return nil +} + +// applyGlobalMappings runs every mapper sourced from spec.values.global. +func applyGlobalMappings(globalMap map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + if err := mapHostnameLicense(globalMap, dst); err != nil { + return err + } + if err := mapSize(globalMap, dst); err != nil { + return err + } + if err := mapCustomCACerts(globalMap, dst); err != nil { + return err + } + if err := mapOIDC(globalMap, dst); err != nil { + return err + } + if err := mapMySQL(globalMap, dst); err != nil { + return err + } + if err := mapRedis(globalMap, dst); err != nil { + return err + } + if err := mapBucket(globalMap, dst); err != nil { + return err + } + + return nil +} + +func mapCustomCACerts(globalMap map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + if certs, found, err := unstructured.NestedStringSlice(globalMap, "customCACerts"); err != nil { + return fmt.Errorf("spec.values.global.customCACerts: %w", err) + } else if found { + dst.Spec.Global.CustomCACerts = certs + } + + if configMap, found, err := unstructured.NestedString(globalMap, "caCertsConfigMap"); err != nil { + return fmt.Errorf("spec.values.global.caCertsConfigMap: %w", err) + } else if found { + dst.Spec.Global.CACertsConfigMap = configMap + } + + return nil +} + +// mapVersion sets spec.wandb.version from app.image.tag, falling back to +// api.image.tag. +func mapVersion(values map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + if tag, found, err := unstructured.NestedString(values, "app", "image", "tag"); err != nil { + return fmt.Errorf("spec.values.app.image.tag: %w", err) + } else if found && tag != "" { + dst.Spec.Wandb.Version = tag + return nil + } + if tag, found, err := unstructured.NestedString(values, "api", "image", "tag"); err != nil { + return fmt.Errorf("spec.values.api.image.tag: %w", err) + } else if found && tag != "" { + dst.Spec.Wandb.Version = tag + } + return nil +} + +// mapServiceAccountAnnotations maps v1's per-sub-chart ServiceAccount +// annotations to v2's single spec.wandb.serviceAccount.annotations, +// preferring `app` and falling back to `api`. +func mapServiceAccountAnnotations(values map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + anns, err := readServiceAccountAnnotations(values, "app") + if err != nil { + return err + } + if len(anns) == 0 { + anns, err = readServiceAccountAnnotations(values, "api") + if err != nil { + return err + } + } + if len(anns) > 0 { + dst.Spec.Wandb.ServiceAccount.Annotations = anns + } + return nil +} + +// mapInternalJWTIssuer pulls the first entry from global.internalJWTMap (or +// app as fallback) into spec.wandb.internalServiceAuth.oidcIssuer. +func mapInternalJWTIssuer(values map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + issuer, err := readFirstInternalJWTIssuer(values, "global") + if err != nil { + return err + } + if issuer == "" { + issuer, err = readFirstInternalJWTIssuer(values, "app") + if err != nil { + return err + } + } + if issuer != "" { + dst.Spec.Wandb.InternalServiceAuth.OIDCIssuer = issuer + } + return nil +} + +// mapIngress maps v1's ingress section to spec.networking and +// spec.wandb.additionalHostnames. ingress.install and ingress.create both +// default to true in the chart, so an absent or partial ingress block still +// enables networking mode "ingress". +func mapIngress(values map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + ingressMap, _, err := unstructured.NestedMap(values, "ingress") + if err != nil { + return fmt.Errorf("spec.values.ingress: %w", err) + } + + if !boolFromValues(ingressMap, true, "install") || !boolFromValues(ingressMap, true, "create") { + return nil + } + + if dst.Spec.Networking.Mode == "" { + dst.Spec.Networking.Mode = appsv2.NetworkingModeIngress + } + + if class, _, err := unstructured.NestedString(ingressMap, "class"); err != nil { + return fmt.Errorf("spec.values.ingress.class: %w", err) + } else if class != "" { + if dst.Spec.Networking.Ingress == nil { + dst.Spec.Networking.Ingress = &appsv2.IngressConfig{} + } + dst.Spec.Networking.Ingress.IngressClassName = ptr.To(class) + } + + if name, _, err := unstructured.NestedString(ingressMap, "nameOverride"); err != nil { + return fmt.Errorf("spec.values.ingress.nameOverride: %w", err) + } else if name != "" { + if dst.Spec.Networking.Ingress == nil { + dst.Spec.Networking.Ingress = &appsv2.IngressConfig{} + } + dst.Spec.Networking.Ingress.Name = name + } + + if anns, _, err := unstructured.NestedStringMap(ingressMap, "annotations"); err != nil { + return fmt.Errorf("spec.values.ingress.annotations: %w", err) + } else if len(anns) > 0 { + dst.Spec.Networking.Annotations = anns + } + + if hosts, _, err := unstructured.NestedStringSlice(ingressMap, "additionalHosts"); err != nil { + return fmt.Errorf("spec.values.ingress.additionalHosts: %w", err) + } else if len(hosts) > 0 { + dst.Spec.Wandb.AdditionalHostnames = hosts + } + + if name, err := firstIngressTLSSecretName(ingressMap); err != nil { + return err + } else if name != "" { + if dst.Spec.Networking.TLS == nil { + dst.Spec.Networking.TLS = &appsv2.TLSConfig{} + } + dst.Spec.Networking.TLS.SecretName = name + } + + return nil +} + +// firstIngressTLSSecretName returns the secretName on the first ingress.tls +// entry. v2 carries only a single TLS Secret; multi-cert v1 configs collapse +// to the first entry. +func firstIngressTLSSecretName(ingressMap map[string]interface{}) (string, error) { + list, _, err := unstructured.NestedSlice(ingressMap, "tls") + if err != nil { + return "", fmt.Errorf("spec.values.ingress.tls: %w", err) + } + if len(list) == 0 { + return "", nil + } + entry, ok := list[0].(map[string]interface{}) + if !ok { + return "", nil + } + name, _, err := unstructured.NestedString(entry, "secretName") + if err != nil { + return "", fmt.Errorf("spec.values.ingress.tls[0].secretName: %w", err) + } + return name, nil +} + +// boolFromValues reads a bool at path within m, returning def when the key +// is absent or the value isn't a bool. Used to apply v1 chart defaults +// during conversion. +func boolFromValues(m map[string]interface{}, def bool, path ...string) bool { + if m == nil { + return def + } + v, found, err := unstructured.NestedBool(m, path...) + if err != nil || !found { + return def + } + return v +} + +// readFirstInternalJWTIssuer returns the issuer on the first internalJWTMap +// entry under values.<service>, or "" when the list is missing or empty. +func readFirstInternalJWTIssuer(values map[string]interface{}, service string) (string, error) { + list, found, err := unstructured.NestedSlice(values, service, "internalJWTMap") + if err != nil { + return "", fmt.Errorf("spec.values.%s.internalJWTMap: %w", service, err) + } + if !found || len(list) == 0 { + return "", nil + } + entry, ok := list[0].(map[string]interface{}) + if !ok { + return "", nil + } + issuer, _, err := unstructured.NestedString(entry, "issuer") + if err != nil { + return "", fmt.Errorf("spec.values.%s.internalJWTMap[0].issuer: %w", service, err) + } + return issuer, nil +} + +// readServiceAccountAnnotations reads values.<service>.serviceAccount.annotations. +func readServiceAccountAnnotations(values map[string]interface{}, service string) (map[string]string, error) { + anns, found, err := unstructured.NestedStringMap(values, service, "serviceAccount", "annotations") + if err != nil { + return nil, fmt.Errorf("spec.values.%s.serviceAccount.annotations: %w", service, err) + } + if !found { + return nil, nil + } + return anns, nil +} + +func mapHostnameLicense(globalMap map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + host, _, err := unstructured.NestedString(globalMap, "host") + if err != nil { + return fmt.Errorf("spec.values.global.host: %w", err) + } + if host != "" { + dst.Spec.Wandb.Hostname = host + } + + license, _, err := unstructured.NestedString(globalMap, "license") + if err != nil { + return fmt.Errorf("spec.values.global.license: %w", err) + } + if license != "" { + dst.Spec.Wandb.License = license + } + + return nil +} + +func mapSize(globalMap map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + size, found, err := unstructured.NestedString(globalMap, "size") + if err != nil { + return fmt.Errorf("spec.values.global.size: %w", err) + } + if !found || size == "" { + return nil + } + + mapped, ok := validSizes[size] + if !ok { + return fmt.Errorf("spec.values.global.size: %q is not a valid v2 size (valid: %s)", size, sortedSizeList()) + } + dst.Spec.Size = mapped + return nil +} + +// mapBucket pulls `bucket.secret` directly into externalObjectStore +// .{AccessKey,SecretKey} and merges the remaining bucket + defaultBucket +// literals (bucket wins on collision) into the bucket-pending annotation. +func mapBucket(globalMap map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + bucket, _, err := unstructured.NestedMap(globalMap, "bucket") + if err != nil { + return fmt.Errorf("spec.values.global.bucket: %w", err) + } + defaultBucket, _, err := unstructured.NestedMap(globalMap, "defaultBucket") + if err != nil { + return fmt.Errorf("spec.values.global.defaultBucket: %w", err) + } + if len(bucket) == 0 && len(defaultBucket) == 0 { + return nil + } + + conn := &appsv2.ObjectStoreConnection{} + dst.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{ + appsv2.DefaultInstanceName: {ExternalObjectStore: conn}, + } + + if sec, ok, err := unstructured.NestedMap(bucket, "secret"); err != nil { + return fmt.Errorf("spec.values.global.bucket.secret: %w", err) + } else if ok { + name, _, _ := unstructured.NestedString(sec, "secretName") + if name != "" { + accessKeyName, _, _ := unstructured.NestedString(sec, "accessKeyName") + if accessKeyName == "" { + accessKeyName = defaultBucketAccessKeyName + } + secretKeyName, _, _ := unstructured.NestedString(sec, "secretKeyName") + if secretKeyName == "" { + secretKeyName = defaultBucketSecretKeyName + } + conn.AccessKey = corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: accessKeyName, + } + conn.SecretKey = corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: secretKeyName, + } + } + } + + merged := map[string]string{} + for k, v := range defaultBucket { + if s, ok := scalarToString(v); ok && s != "" { + merged[k] = s + } + } + for k, v := range bucket { + if k == "secret" { + continue + } + if s, ok := scalarToString(v); ok && s != "" { + merged[k] = s + } + } + if len(merged) == 0 { + return nil + } + return writeAnnotation(dst, BucketPendingAnnotation, merged) +} + +func writeAnnotation(dst *appsv2.WeightsAndBiases, key string, value interface{}) error { + payload, err := json.Marshal(value) + if err != nil { + return fmt.Errorf("marshal %s: %w", key, err) + } + if dst.Annotations == nil { + dst.Annotations = make(map[string]string) + } + dst.Annotations[key] = string(payload) + return nil +} + +// mysqlFields maps each v1 global.mysql.<key> to a *MysqlConnection setter. +var mysqlFields = []struct { + v1Key string + setRef func(*appsv2.MysqlConnection, corev1.SecretKeySelector) +}{ + {"host", func(c *appsv2.MysqlConnection, s corev1.SecretKeySelector) { c.Host = s }}, + {"port", func(c *appsv2.MysqlConnection, s corev1.SecretKeySelector) { c.Port = s }}, + {"database", func(c *appsv2.MysqlConnection, s corev1.SecretKeySelector) { c.Database = s }}, + {"user", func(c *appsv2.MysqlConnection, s corev1.SecretKeySelector) { c.Username = s }}, + {"password", func(c *appsv2.MysqlConnection, s corev1.SecretKeySelector) { c.Password = s }}, + {"caCert", func(c *appsv2.MysqlConnection, s corev1.SecretKeySelector) { c.SslCa = s }}, +} + +// mapMySQL routes valueFrom-shaped fields to externalMysql.*, scalars to +// the mysql-pending annotation, and the legacy passwordSecret block to +// externalMysql.password (skipped when password.valueFrom already won). +func mapMySQL(globalMap map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + mysqlMap, found, err := unstructured.NestedMap(globalMap, "mysql") + if err != nil { + return fmt.Errorf("spec.values.global.mysql: %w", err) + } + if !found || len(mysqlMap) == 0 { + return nil + } + + conn := &appsv2.MysqlConnection{} + + remaining := map[string]string{} + + for _, f := range mysqlFields { + raw, ok := mysqlMap[f.v1Key] + if !ok { + continue + } + ref, literal, classifyErr := classifyValueFromOrLiteral(raw) + if classifyErr != nil { + return fmt.Errorf("spec.values.global.mysql.%s: %w", f.v1Key, classifyErr) + } + switch { + case ref != nil: + f.setRef(conn, *ref) + case literal != "": + remaining[f.v1Key] = literal + } + } + + if ps, ok, err := unstructured.NestedMap(mysqlMap, "passwordSecret"); err != nil { + return fmt.Errorf("spec.values.global.mysql.passwordSecret: %w", err) + } else if ok { + name, _, _ := unstructured.NestedString(ps, "name") + alreadyHasPassword := conn != nil && conn.Password.Name != "" + if name != "" && !alreadyHasPassword { + key, _, _ := unstructured.NestedString(ps, "passwordKey") + if key == "" { + key = defaultMySQLPasswordSecretKey + } + conn.Password = corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: key, + } + delete(remaining, "password") + } + } + + dst.Spec.MySQL = map[string]appsv2.MySQLSpec{ + appsv2.DefaultInstanceName: {ExternalMysql: conn}, + } + + if len(remaining) > 0 { + return writeAnnotation(dst, MySQLPendingAnnotation, remaining) + } + return nil +} + +// redisFields maps each v1 global.redis.<key> to a *RedisConnection setter. +var redisFields = []struct { + v1Key string + setRef func(*appsv2.RedisConnection, corev1.SecretKeySelector) +}{ + {"host", func(c *appsv2.RedisConnection, s corev1.SecretKeySelector) { c.Host = s }}, + {"port", func(c *appsv2.RedisConnection, s corev1.SecretKeySelector) { c.Port = s }}, + {"password", func(c *appsv2.RedisConnection, s corev1.SecretKeySelector) { c.Password = s }}, + {"caCert", func(c *appsv2.RedisConnection, s corev1.SecretKeySelector) { c.SslCa = s }}, +} + +// mapRedis routes valueFrom-shaped fields to externalRedis.*, scalars to +// the redis-pending annotation, and the legacy secret block to +// externalRedis.password (skipped when password.valueFrom already won). +// Unknown fields (external, parameters, params) have no v2 home and are +// dropped. +func mapRedis(globalMap map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + redisMap, found, err := unstructured.NestedMap(globalMap, "redis") + if err != nil { + return fmt.Errorf("spec.values.global.redis: %w", err) + } + if !found || len(redisMap) == 0 { + return nil + } + + conn := &appsv2.RedisConnection{} + + remaining := map[string]string{} + + for _, f := range redisFields { + raw, ok := redisMap[f.v1Key] + if !ok { + continue + } + ref, literal, classifyErr := classifyValueFromOrLiteral(raw) + if classifyErr != nil { + return fmt.Errorf("spec.values.global.redis.%s: %w", f.v1Key, classifyErr) + } + switch { + case ref != nil: + f.setRef(conn, *ref) + case literal != "": + remaining[f.v1Key] = literal + } + } + + // tls is nested under redis.params (preferred) or redis.parameters in v1. + for _, parent := range []string{"params", "parameters"} { + raw, found, err := unstructured.NestedFieldNoCopy(redisMap, parent, "tls") + if err != nil { + return fmt.Errorf("spec.values.global.redis.%s.tls: %w", parent, err) + } + if !found { + continue + } + ref, literal, classifyErr := classifyValueFromOrLiteral(raw) + if classifyErr != nil { + return fmt.Errorf("spec.values.global.redis.%s.tls: %w", parent, classifyErr) + } + if ref != nil { + conn.Tls = *ref + break + } + if literal != "" { + remaining["tls"] = literal + break + } + } + + if sec, ok, err := unstructured.NestedMap(redisMap, "secret"); err != nil { + return fmt.Errorf("spec.values.global.redis.secret: %w", err) + } else if ok { + name, _, _ := unstructured.NestedString(sec, "secretName") + alreadyHasPassword := conn != nil && conn.Password.Name != "" + if name != "" && !alreadyHasPassword { + key, _, _ := unstructured.NestedString(sec, "secretKey") + if key == "" { + key = defaultRedisPasswordSecretKey + } + conn.Password = corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: key, + } + delete(remaining, "password") + } + } + + dst.Spec.Redis = map[string]appsv2.RedisSpec{ + appsv2.DefaultInstanceName: {ExternalRedis: conn}, + } + + if len(remaining) > 0 { + return writeAnnotation(dst, RedisPendingAnnotation, remaining) + } + return nil +} + +// oidcFields maps each v1 global.auth.oidc.<key> to an *OidcSpec setter. +// v1's `secret` is the client secret; `issuer` is the issuer URL. +var oidcFields = []struct { + v1Key string + setRef func(*appsv2.OidcSpec, corev1.SecretKeySelector) +}{ + {"clientId", func(o *appsv2.OidcSpec, s corev1.SecretKeySelector) { o.ClientId = s }}, + {"secret", func(o *appsv2.OidcSpec, s corev1.SecretKeySelector) { o.ClientSecret = s }}, + {"authMethod", func(o *appsv2.OidcSpec, s corev1.SecretKeySelector) { o.AuthMethod = s }}, + {"issuer", func(o *appsv2.OidcSpec, s corev1.SecretKeySelector) { o.IssuerUrl = s }}, +} + +// mapOIDC routes valueFrom-shaped fields to spec.wandb.oidc.*, scalars to +// the oidc-pending annotation, and the legacy oidcSecret block to +// oidc.clientSecret (skipped when secret.valueFrom already won). +func mapOIDC(globalMap map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + oidcMap, found, err := unstructured.NestedMap(globalMap, "auth", "oidc") + if err != nil { + return fmt.Errorf("spec.values.global.auth.oidc: %w", err) + } + if !found || len(oidcMap) == 0 { + return nil + } + + oidc := &dst.Spec.Wandb.OIDC + remaining := map[string]string{} + + for _, f := range oidcFields { + raw, ok := oidcMap[f.v1Key] + if !ok { + continue + } + ref, literal, classifyErr := classifyValueFromOrLiteral(raw) + if classifyErr != nil { + return fmt.Errorf("spec.values.global.auth.oidc.%s: %w", f.v1Key, classifyErr) + } + switch { + case ref != nil: + f.setRef(oidc, *ref) + case literal != "": + remaining[f.v1Key] = literal + } + } + + if os, ok, err := unstructured.NestedMap(oidcMap, "oidcSecret"); err != nil { + return fmt.Errorf("spec.values.global.auth.oidc.oidcSecret: %w", err) + } else if ok { + name, _, _ := unstructured.NestedString(os, "name") + alreadyHasClientSecret := oidc.ClientSecret.Name != "" + if name != "" && !alreadyHasClientSecret { + key, _, _ := unstructured.NestedString(os, "secretKey") + if key == "" { + key = defaultOIDCClientSecretKey + } + oidc.ClientSecret = corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: key, + } + delete(remaining, "secret") + } + } + + if len(remaining) > 0 { + return writeAnnotation(dst, OIDCPendingAnnotation, remaining) + } + return nil +} + +// classifyValueFromOrLiteral returns a SecretKeySelector when raw is +// {valueFrom: {secretKeyRef: {name, key}}}, otherwise the scalar stringified +// as a literal, otherwise ("", no literal). Malformed ref maps error out. +// The literal is always a string so everything stashed in the pending +// annotations (which the reconciler materializes into Secret data) is a +// string regardless of the v1 value's JSON type. +func classifyValueFromOrLiteral(raw interface{}) (*corev1.SecretKeySelector, string, error) { + if m, ok := raw.(map[string]interface{}); ok { + name, _, _ := unstructured.NestedString(m, "valueFrom", "secretKeyRef", "name") + key, _, _ := unstructured.NestedString(m, "valueFrom", "secretKeyRef", "key") + if name == "" || key == "" { + return nil, "", fmt.Errorf("map value must be {valueFrom: {secretKeyRef: {name, key}}}") + } + return &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: key, + }, "", nil + } + s, _ := scalarToString(raw) + return nil, s, nil +} + +// scalarToString renders a JSON scalar (string, bool, number) as a string. +// Returns ("", false) for nil and non-scalar values (maps, slices). +func scalarToString(v interface{}) (string, bool) { + switch t := v.(type) { + case nil: + return "", false + case string: + return t, true + case bool: + return strconv.FormatBool(t), true + case json.Number: + return t.String(), true + case float64: + if t == float64(int64(t)) { + return strconv.FormatInt(int64(t), 10), true + } + return strconv.FormatFloat(t, 'f', -1, 64), true + case int64: + return strconv.FormatInt(t, 10), true + case int: + return strconv.Itoa(t), true + default: + return "", false + } +} + +func sortedSizeList() string { + names := make([]string, 0, len(validSizes)) + for k := range validSizes { + names = append(names, k) + } + sort.Strings(names) + return strings.Join(names, ", ") +} diff --git a/api/v1/weightsandbiases_conversion_overrides.go b/api/v1/weightsandbiases_conversion_overrides.go new file mode 100644 index 00000000..204d3714 --- /dev/null +++ b/api/v1/weightsandbiases_conversion_overrides.go @@ -0,0 +1,367 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "sort" + "strings" + "sync" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + appsv2 "github.com/wandb/operator/api/v2" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" +) + +const ( + legacyDefaultSize = "small" + + manifestFetchTimeout = 15 * time.Second + manifestFailureCooldown = time.Minute +) + +// conversionManifestGetter is a test seam over the real manifest resolver. +var conversionManifestGetter = serverManifest.GetServerManifest + +// Failure cooldown per (repository, version): GetServerManifest caches +// successes in its on-disk ORAS store but retries the remote on every failure, +// which would stall each v1 write for the fetch timeout. +var ( + manifestFailuresMu sync.Mutex + manifestFailures = map[string]manifestFailure{} +) + +type manifestFailure struct { + err error + until time.Time +} + +// SetConversionManifestGetter swaps the resolver and clears the failure +// cooldowns. For tests; nil restores the default. +func SetConversionManifestGetter(getter func(ctx context.Context, repository, version string) (serverManifest.Manifest, error)) { + manifestFailuresMu.Lock() + defer manifestFailuresMu.Unlock() + if getter == nil { + getter = serverManifest.GetServerManifest + } + conversionManifestGetter = getter + manifestFailures = map[string]manifestFailure{} +} + +// legacyManifestApps maps each manifest application name to the v1 values key +// holding its section (legacyKey when set, else the name). v1 values carry no +// repository field, so the defaulting webhook's default repository is used. +func legacyManifestApps(version string) (map[string]string, error) { + repository := appsv2.DefaultManifestRepository + key := repository + "|" + version + + manifestFailuresMu.Lock() + getter := conversionManifestGetter + if failure, ok := manifestFailures[key]; ok && time.Now().Before(failure.until) { + manifestFailuresMu.Unlock() + return nil, failure.err + } + manifestFailuresMu.Unlock() + + ctx, cancel := context.WithTimeout(context.Background(), manifestFetchTimeout) + defer cancel() + + m, err := getter(ctx, repository, version) + if err != nil { + manifestFailuresMu.Lock() + manifestFailures[key] = manifestFailure{err: err, until: time.Now().Add(manifestFailureCooldown)} + manifestFailuresMu.Unlock() + return nil, err + } + + apps := make(map[string]string, len(m.Applications)) + for name, app := range m.Applications { + valuesKey := app.LegacyKey + if valuesKey == "" { + valuesKey = name + } + apps[name] = valuesKey + } + return apps, nil +} + +// mapLegacyOverrides extracts global and per-application env/extraEnv and +// resource overrides from v1 values into spec.wandb.legacyOverrides, with the +// server manifest for the converted version (set by mapVersion earlier) as +// the authority on which sections are applications. +func mapLegacyOverrides(values map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + overrides := map[string]appsv2.LegacyOverrides{} + + globalMap, _, err := unstructured.NestedMap(values, "global") + if err != nil { + return fmt.Errorf("spec.values.global: %w", err) + } + if len(globalMap) > 0 { + env, err := legacyEnvFromSection(globalMap, "global") + if err != nil { + return err + } + if len(env) > 0 { + overrides[appsv2.LegacyOverridesGlobalKey] = appsv2.LegacyOverrides{Env: env} + } + } + + globalSize, _, err := unstructured.NestedString(globalMap, "size") + if err != nil { + return fmt.Errorf("spec.values.global.size: %w", err) + } + + if err := mapPerAppLegacyOverrides(values, dst.Spec.Wandb.Version, globalSize, overrides); err != nil { + return err + } + + if len(overrides) > 0 { + dst.Spec.Wandb.LegacyOverrides = overrides + } + return nil +} + +// mapPerAppLegacyOverrides is best-effort: a manifest fetch failure must never +// make v1 objects unservable, so it logs and skips instead of erroring. +func mapPerAppLegacyOverrides(values map[string]interface{}, version, globalSize string, overrides map[string]appsv2.LegacyOverrides) error { + if version == "" { + logger.Info("no version derived from v1 values; skipping per-application legacy overrides") + return nil + } + apps, err := legacyManifestApps(version) + if err != nil { + logger.Error(err, "failed to resolve server manifest; skipping per-application legacy overrides", + "version", version) + return nil + } + + appNames := make([]string, 0, len(apps)) + for name := range apps { + appNames = append(appNames, name) + } + sort.Strings(appNames) + + for _, name := range appNames { + key := apps[name] + section, found, err := unstructured.NestedMap(values, key) + if err != nil { + return fmt.Errorf("spec.values.%s: %w", key, err) + } + if !found { + continue + } + + env, err := legacyEnvFromSection(section, key) + if err != nil { + return err + } + resources, err := legacyResourcesFromSection(section, key, globalSize) + if err != nil { + return err + } + if len(env) == 0 && resources == nil { + continue + } + overrides[name] = appsv2.LegacyOverrides{Env: env, Resources: resources} + } + + logUnmappedLegacySections(values, apps, version) + return nil +} + +// logUnmappedLegacySections logs override-shaped sections no manifest +// application reads (e.g. the v1 monolith `app`, `console`) — not converted, +// preserved only in the v1-values annotation. +func logUnmappedLegacySections(values map[string]interface{}, apps map[string]string, version string) { + mappedKeys := make(map[string]struct{}, len(apps)) + for _, valuesKey := range apps { + mappedKeys[valuesKey] = struct{}{} + } + + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + sort.Strings(keys) + + for _, key := range keys { + if key == "global" { + continue + } + if _, ok := mappedKeys[key]; ok { + continue + } + section, ok := values[key].(map[string]interface{}) + if !ok || !hasLegacyOverrideShape(section) { + continue + } + logger.Info("legacy values section does not map to any application in the server manifest; its env/resources are not converted", + "section", key, "version", version) + } +} + +// hasLegacyOverrideShape reports whether a section carries keys the extraction +// reads. Flat `resources` is excluded: infra sections (mysql, redis, …) +// legitimately carry it. +func hasLegacyOverrideShape(section map[string]interface{}) bool { + for _, key := range []string{"env", "extraEnv", "sizing"} { + if _, ok := section[key]; ok { + return true + } + } + return false +} + +// legacyEnvFromSection merges env over extraEnv (the chart's precedence) into +// a name-sorted EnvVar list, keeping round-trips deterministic. +func legacyEnvFromSection(section map[string]interface{}, sectionName string) ([]corev1.EnvVar, error) { + merged := map[string]interface{}{} + for _, sub := range []string{"extraEnv", "env"} { + m, found, err := unstructured.NestedMap(section, sub) + if err != nil { + return nil, fmt.Errorf("spec.values.%s.%s: %w", sectionName, sub, err) + } + if !found { + continue + } + for k, v := range m { + merged[k] = v + } + } + if len(merged) == 0 { + return nil, nil + } + + vars := make([]corev1.EnvVar, 0, len(merged)) + for name, raw := range merged { + envVar, ok, err := legacyEnvVar(name, raw, sectionName) + if err != nil { + return nil, err + } + if ok { + vars = append(vars, envVar) + } + } + if len(vars) == 0 { + return nil, nil + } + sort.Slice(vars, func(i, j int) bool { return vars[i].Name < vars[j].Name }) + return vars, nil +} + +// legacyEnvVar renders one helm env entry: map values decode strictly as +// EnvVar bodies (malformed fails conversion, like other mappers), scalars +// coerce as helm's toString did, and `{{ }}` templates drop with a log. +func legacyEnvVar(name string, raw interface{}, sectionName string) (corev1.EnvVar, bool, error) { + if body, isMap := raw.(map[string]interface{}); isMap { + payload, err := json.Marshal(body) + if err != nil { + return corev1.EnvVar{}, false, fmt.Errorf("spec.values.%s env %s: %w", sectionName, name, err) + } + dec := json.NewDecoder(bytes.NewReader(payload)) + dec.DisallowUnknownFields() + var envVar corev1.EnvVar + if err := dec.Decode(&envVar); err != nil { + return corev1.EnvVar{}, false, fmt.Errorf("spec.values.%s env %s: %w", sectionName, name, err) + } + envVar.Name = name + if strings.Contains(envVar.Value, "{{") { + logger.Info("dropping legacy env var with helm template value", + "section", sectionName, "name", name) + return corev1.EnvVar{}, false, nil + } + return envVar, true, nil + } + + s, ok := scalarToString(raw) + if !ok { + logger.Info("dropping legacy env var with non-scalar value", + "section", sectionName, "name", name) + return corev1.EnvVar{}, false, nil + } + if strings.Contains(s, "{{") { + logger.Info("dropping legacy env var with helm template value", + "section", sectionName, "name", name) + return corev1.EnvVar{}, false, nil + } + return corev1.EnvVar{Name: name, Value: s}, true, nil +} + +// legacyResourcesFromSection deep-merges sizing.default → sizing.<effective +// size> → flat resources, mirroring the chart. Sections that set nothing +// yield nil so v2 manifest sizing applies untouched. +func legacyResourcesFromSection(section map[string]interface{}, sectionName, globalSize string) (*corev1.ResourceRequirements, error) { + size, _, err := unstructured.NestedString(section, "size") + if err != nil { + return nil, fmt.Errorf("spec.values.%s.size: %w", sectionName, err) + } + if size == "" { + size = globalSize + } + if size == "" { + size = legacyDefaultSize + } + + merged := map[string]interface{}{} + for _, path := range [][]string{ + {"sizing", "default", "resources"}, + {"sizing", size, "resources"}, + {"resources"}, + } { + m, found, err := unstructured.NestedMap(section, path...) + if err != nil { + return nil, fmt.Errorf("spec.values.%s.%s: %w", sectionName, strings.Join(path, "."), err) + } + if found { + mergeLegacyValueMaps(merged, m) + } + } + if len(merged) == 0 { + return nil, nil + } + + payload, err := json.Marshal(merged) + if err != nil { + return nil, fmt.Errorf("spec.values.%s resources: %w", sectionName, err) + } + dec := json.NewDecoder(bytes.NewReader(payload)) + dec.DisallowUnknownFields() + var resources corev1.ResourceRequirements + if err := dec.Decode(&resources); err != nil { + return nil, fmt.Errorf("spec.values.%s resources: %w", sectionName, err) + } + return &resources, nil +} + +// mergeLegacyValueMaps deep-merges src into dst like helm merges values maps. +func mergeLegacyValueMaps(dst, src map[string]interface{}) { + for k, v := range src { + if srcMap, ok := v.(map[string]interface{}); ok { + if dstMap, ok := dst[k].(map[string]interface{}); ok { + mergeLegacyValueMaps(dstMap, srcMap) + continue + } + } + dst[k] = v + } +} diff --git a/api/v1/weightsandbiases_conversion_overrides_test.go b/api/v1/weightsandbiases_conversion_overrides_test.go new file mode 100644 index 00000000..c85734af --- /dev/null +++ b/api/v1/weightsandbiases_conversion_overrides_test.go @@ -0,0 +1,495 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "errors" + "os" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + + appsv2 "github.com/wandb/operator/api/v2" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" +) + +const testLegacyVersion = "0.83.0-test" + +// disableConversionManifestFetch keeps unit tests off the network; tests opt +// in via withConversionManifest*. +func disableConversionManifestFetch() { + SetConversionManifestGetter(func(_ context.Context, _, _ string) (serverManifest.Manifest, error) { + return serverManifest.Manifest{}, errors.New("manifest fetch disabled in unit tests") + }) +} + +func TestMain(m *testing.M) { + disableConversionManifestFetch() + os.Exit(m.Run()) +} + +// withConversionManifest installs a fake resolver returning the given +// applications and returns its call counter. +func withConversionManifest(t *testing.T, apps map[string]serverManifest.Application) *atomic.Int32 { + t.Helper() + var calls atomic.Int32 + SetConversionManifestGetter(func(_ context.Context, _, _ string) (serverManifest.Manifest, error) { + calls.Add(1) + return serverManifest.Manifest{Applications: apps}, nil + }) + t.Cleanup(disableConversionManifestFetch) + return &calls +} + +// withConversionManifestApps is withConversionManifest for plain names with no +// legacyKey. +func withConversionManifestApps(t *testing.T, names ...string) *atomic.Int32 { + t.Helper() + apps := make(map[string]serverManifest.Application, len(names)) + for _, name := range names { + apps[name] = serverManifest.Application{Name: name} + } + return withConversionManifest(t, apps) +} + +// withVersion adds the app.image.tag mapVersion reads, so per-app extraction +// has a version to resolve the manifest with. +func withVersion(values map[string]interface{}) map[string]interface{} { + values["app"] = map[string]interface{}{ + "image": map[string]interface{}{"tag": testLegacyVersion}, + } + return values +} + +func TestConvertTo_LegacyOverridesAbsent(t *testing.T) { + withConversionManifestApps(t, "api") + dst := &appsv2.WeightsAndBiases{} + src := newV1(withVersion(map[string]interface{}{ + "global": map[string]interface{}{"host": "https://wandb.example.com"}, + })) + require.NoError(t, src.ConvertTo(dst)) + require.Nil(t, dst.Spec.Wandb.LegacyOverrides) +} + +func TestConvertTo_LegacyOverridesGlobalEnvPrecedence(t *testing.T) { + // No version in values: global env must convert without any manifest fetch. + SetConversionManifestGetter(func(_ context.Context, _, _ string) (serverManifest.Manifest, error) { + t.Fatal("manifest must not be resolved when no version is derived") + return serverManifest.Manifest{}, nil + }) + t.Cleanup(disableConversionManifestFetch) + + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "env": map[string]interface{}{ + "BOTH": "from-env", + "ENV_ONLY": "env-value", + }, + "extraEnv": map[string]interface{}{ + "BOTH": "from-extra-env", + "EXTRA_ONLY": "extra-value", + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.Contains(t, dst.Spec.Wandb.LegacyOverrides, appsv2.LegacyOverridesGlobalKey) + global := dst.Spec.Wandb.LegacyOverrides[appsv2.LegacyOverridesGlobalKey] + require.Equal(t, []corev1.EnvVar{ + {Name: "BOTH", Value: "from-env"}, + {Name: "ENV_ONLY", Value: "env-value"}, + {Name: "EXTRA_ONLY", Value: "extra-value"}, + }, global.Env) + require.Nil(t, global.Resources) +} + +func TestConvertTo_LegacyOverridesScalarCoercion(t *testing.T) { + withConversionManifestApps(t, "parquet") + dst := &appsv2.WeightsAndBiases{} + src := newV1(withVersion(map[string]interface{}{ + "parquet": map[string]interface{}{ + "env": map[string]interface{}{ + "BOOL": true, + "INT": int64(8083), + "FLOAT": 1.5, + "STR": "plain", + }, + }, + })) + require.NoError(t, src.ConvertTo(dst)) + + require.Equal(t, []corev1.EnvVar{ + {Name: "BOOL", Value: "true"}, + {Name: "FLOAT", Value: "1.5"}, + {Name: "INT", Value: "8083"}, + {Name: "STR", Value: "plain"}, + }, dst.Spec.Wandb.LegacyOverrides["parquet"].Env) +} + +func TestConvertTo_LegacyOverridesValueFromBody(t *testing.T) { + withConversionManifestApps(t, "api") + dst := &appsv2.WeightsAndBiases{} + src := newV1(withVersion(map[string]interface{}{ + "api": map[string]interface{}{ + "env": map[string]interface{}{ + "API_KEY": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "observability", + "key": "api-key", + }, + }, + }, + }, + }, + })) + require.NoError(t, src.ConvertTo(dst)) + + env := dst.Spec.Wandb.LegacyOverrides["api"].Env + require.Len(t, env, 1) + require.Equal(t, "API_KEY", env[0].Name) + require.NotNil(t, env[0].ValueFrom) + require.NotNil(t, env[0].ValueFrom.SecretKeyRef) + require.Equal(t, "observability", env[0].ValueFrom.SecretKeyRef.Name) + require.Equal(t, "api-key", env[0].ValueFrom.SecretKeyRef.Key) +} + +func TestConvertTo_LegacyOverridesMalformedBodyFails(t *testing.T) { + withConversionManifestApps(t, "api") + dst := &appsv2.WeightsAndBiases{} + src := newV1(withVersion(map[string]interface{}{ + "api": map[string]interface{}{ + "env": map[string]interface{}{ + "BROKEN": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyReff": map[string]interface{}{"name": "x", "key": "y"}, + }, + }, + }, + }, + })) + err := src.ConvertTo(dst) + require.Error(t, err) + require.Contains(t, err.Error(), "spec.values.api env BROKEN") +} + +func TestConvertTo_LegacyOverridesTemplateValuesDropped(t *testing.T) { + withConversionManifestApps(t, "executor") + dst := &appsv2.WeightsAndBiases{} + src := newV1(withVersion(map[string]interface{}{ + "global": map[string]interface{}{ + "extraEnv": map[string]interface{}{ + "TEMPLATED": "{{ .Release.Name }}-suffix", + "KEPT": "plain", + "INTERP": "$(OTHER_VAR)/path", + }, + }, + "executor": map[string]interface{}{ + "env": map[string]interface{}{ + "ONLY_TEMPLATED": `{{ include "wandb.executor.taskQueue" . }}`, + }, + }, + })) + require.NoError(t, src.ConvertTo(dst)) + + global := dst.Spec.Wandb.LegacyOverrides[appsv2.LegacyOverridesGlobalKey] + require.Equal(t, []corev1.EnvVar{ + {Name: "INTERP", Value: "$(OTHER_VAR)/path"}, + {Name: "KEPT", Value: "plain"}, + }, global.Env) + + // executor's only entry was templated, so the whole section is absent. + require.NotContains(t, dst.Spec.Wandb.LegacyOverrides, "executor") +} + +func TestConvertTo_LegacyOverridesManifestLegacyKey(t *testing.T) { + // Renamed apps declare their v1 values key via legacyKey; no rename table. + withConversionManifest(t, map[string]serverManifest.Application{ + "nginx-proxy": {Name: "nginx-proxy", LegacyKey: "nginx"}, + "weave-trace-evaluate-model-worker": { + Name: "weave-trace-evaluate-model-worker", + LegacyKey: "weave-evaluate-model-worker", + }, + "parquet": {Name: "parquet"}, + }) + dst := &appsv2.WeightsAndBiases{} + src := newV1(withVersion(map[string]interface{}{ + "nginx": map[string]interface{}{ + "env": map[string]interface{}{"NGINX_VAR": "1"}, + }, + "weave-evaluate-model-worker": map[string]interface{}{ + "env": map[string]interface{}{"WORKER_VAR": "2"}, + }, + "parquet": map[string]interface{}{ + "env": map[string]interface{}{"PARQUET_VAR": "3"}, + }, + })) + require.NoError(t, src.ConvertTo(dst)) + + overrides := dst.Spec.Wandb.LegacyOverrides + require.Contains(t, overrides, "nginx-proxy") + require.NotContains(t, overrides, "nginx") + require.Contains(t, overrides, "weave-trace-evaluate-model-worker") + require.NotContains(t, overrides, "weave-evaluate-model-worker") + require.Contains(t, overrides, "parquet") +} + +func TestConvertTo_LegacyOverridesWithoutLegacyKeyRenamedSectionSkipped(t *testing.T) { + // Manifest predating legacyKey: the nginx section has no reader, so it is + // logged as unmapped and skipped rather than guessed. + withConversionManifestApps(t, "nginx-proxy") + dst := &appsv2.WeightsAndBiases{} + src := newV1(withVersion(map[string]interface{}{ + "nginx": map[string]interface{}{ + "env": map[string]interface{}{"NGINX_VAR": "1"}, + }, + })) + require.NoError(t, src.ConvertTo(dst)) + require.NotContains(t, dst.Spec.Wandb.LegacyOverrides, "nginx-proxy") + require.NotContains(t, dst.Spec.Wandb.LegacyOverrides, "nginx") +} + +func TestConvertTo_LegacyOverridesUnmappedSectionsSkipped(t *testing.T) { + withConversionManifestApps(t, "api") + dst := &appsv2.WeightsAndBiases{} + values := withVersion(map[string]interface{}{ + "console": map[string]interface{}{ + "env": map[string]interface{}{"CONSOLE_VAR": "2"}, + }, + "api": map[string]interface{}{ + "env": map[string]interface{}{"API_VAR": "1"}, + }, + }) + // The monolith section carries env alongside the image tag withVersion set. + values["app"].(map[string]interface{})["env"] = map[string]interface{}{"MONOLITH_VAR": "1"} + src := newV1(values) + require.NoError(t, src.ConvertTo(dst)) + + // Only manifest applications convert; app/console are logged and skipped. + overrides := dst.Spec.Wandb.LegacyOverrides + require.Equal(t, []corev1.EnvVar{{Name: "API_VAR", Value: "1"}}, overrides["api"].Env) + require.NotContains(t, overrides, "app") + require.NotContains(t, overrides, "console") +} + +func TestConvertTo_LegacyOverridesResourcesSizingMerge(t *testing.T) { + withConversionManifestApps(t, "api") + dst := &appsv2.WeightsAndBiases{} + src := newV1(withVersion(map[string]interface{}{ + "global": map[string]interface{}{"size": "medium"}, + "api": map[string]interface{}{ + "sizing": map[string]interface{}{ + "default": map[string]interface{}{ + "resources": map[string]interface{}{ + "requests": map[string]interface{}{"cpu": "100m", "memory": "128Mi"}, + }, + }, + "medium": map[string]interface{}{ + "resources": map[string]interface{}{ + "requests": map[string]interface{}{"cpu": "2"}, + "limits": map[string]interface{}{"memory": "4Gi"}, + }, + }, + "small": map[string]interface{}{ + "resources": map[string]interface{}{ + "requests": map[string]interface{}{"cpu": "should-not-apply"}, + }, + }, + }, + "resources": map[string]interface{}{ + "limits": map[string]interface{}{"cpu": "3"}, + }, + }, + })) + require.NoError(t, src.ConvertTo(dst)) + + resources := dst.Spec.Wandb.LegacyOverrides["api"].Resources + require.NotNil(t, resources) + // medium requests.cpu beat default; default memory request survives; + // flat resources.limits merged over sizing limits. + require.Equal(t, resource.MustParse("2"), resources.Requests[corev1.ResourceCPU]) + require.Equal(t, resource.MustParse("128Mi"), resources.Requests[corev1.ResourceMemory]) + require.Equal(t, resource.MustParse("3"), resources.Limits[corev1.ResourceCPU]) + require.Equal(t, resource.MustParse("4Gi"), resources.Limits[corev1.ResourceMemory]) +} + +func TestConvertTo_LegacyOverridesResourcesPerAppSizeWins(t *testing.T) { + withConversionManifestApps(t, "parquet") + dst := &appsv2.WeightsAndBiases{} + src := newV1(withVersion(map[string]interface{}{ + "global": map[string]interface{}{"size": "small"}, + "parquet": map[string]interface{}{ + "size": "xlarge", + "sizing": map[string]interface{}{ + "small": map[string]interface{}{ + "resources": map[string]interface{}{ + "requests": map[string]interface{}{"cpu": "1"}, + }, + }, + "xlarge": map[string]interface{}{ + "resources": map[string]interface{}{ + "requests": map[string]interface{}{"cpu": "8"}, + }, + }, + }, + }, + })) + require.NoError(t, src.ConvertTo(dst)) + + resources := dst.Spec.Wandb.LegacyOverrides["parquet"].Resources + require.NotNil(t, resources) + require.Equal(t, resource.MustParse("8"), resources.Requests[corev1.ResourceCPU]) +} + +func TestConvertTo_LegacyOverridesResourcesDefaultSizeIsSmall(t *testing.T) { + withConversionManifestApps(t, "weave") + dst := &appsv2.WeightsAndBiases{} + src := newV1(withVersion(map[string]interface{}{ + "weave": map[string]interface{}{ + "sizing": map[string]interface{}{ + "small": map[string]interface{}{ + "resources": map[string]interface{}{ + "requests": map[string]interface{}{"memory": "1Gi"}, + }, + }, + "large": map[string]interface{}{ + "resources": map[string]interface{}{ + "requests": map[string]interface{}{"memory": "16Gi"}, + }, + }, + }, + }, + })) + require.NoError(t, src.ConvertTo(dst)) + + resources := dst.Spec.Wandb.LegacyOverrides["weave"].Resources + require.NotNil(t, resources) + require.Equal(t, resource.MustParse("1Gi"), resources.Requests[corev1.ResourceMemory]) +} + +func TestConvertTo_LegacyOverridesManifestUnavailable(t *testing.T) { + SetConversionManifestGetter(func(_ context.Context, _, _ string) (serverManifest.Manifest, error) { + return serverManifest.Manifest{}, errors.New("registry unreachable") + }) + t.Cleanup(disableConversionManifestFetch) + + dst := &appsv2.WeightsAndBiases{} + src := newV1(withVersion(map[string]interface{}{ + "global": map[string]interface{}{ + "env": map[string]interface{}{"HTTP_PROXY": "http://proxy"}, + }, + "api": map[string]interface{}{ + "env": map[string]interface{}{"API_VAR": "1"}, + }, + })) + // A manifest fetch failure must never fail conversion: global env still + // converts, per-app extraction is skipped. + require.NoError(t, src.ConvertTo(dst)) + + overrides := dst.Spec.Wandb.LegacyOverrides + require.Contains(t, overrides, appsv2.LegacyOverridesGlobalKey) + require.NotContains(t, overrides, "api") +} + +func TestConvertTo_LegacyOverridesManifestFailureCooldown(t *testing.T) { + // The cooldown keeps repeat conversions from stalling on an unreachable registry. + var calls atomic.Int32 + SetConversionManifestGetter(func(_ context.Context, _, _ string) (serverManifest.Manifest, error) { + calls.Add(1) + return serverManifest.Manifest{}, errors.New("registry unreachable") + }) + t.Cleanup(disableConversionManifestFetch) + + for i := 0; i < 3; i++ { + dst := &appsv2.WeightsAndBiases{} + src := newV1(withVersion(map[string]interface{}{ + "api": map[string]interface{}{ + "env": map[string]interface{}{"API_VAR": "1"}, + }, + })) + require.NoError(t, src.ConvertTo(dst)) + require.NotContains(t, dst.Spec.Wandb.LegacyOverrides, "api") + } + + require.Equal(t, int32(1), calls.Load(), "repeat conversions within the cooldown must not retry the fetch") +} + +func TestConvertTo_LegacyOverridesPrefersActiveSpecValues(t *testing.T) { + withConversionManifestApps(t, "api") + withConversionReader(t, activeSpecSecret(t, "default", "wandb", withVersion(map[string]interface{}{ + "api": map[string]interface{}{ + "env": map[string]interface{}{"FROM_ACTIVE": "yes"}, + }, + }))) + + dst := &appsv2.WeightsAndBiases{} + src := newV1(withVersion(map[string]interface{}{ + "api": map[string]interface{}{ + "env": map[string]interface{}{"FROM_CR": "yes"}, + }, + })) + require.NoError(t, src.ConvertTo(dst)) + + require.Equal(t, []corev1.EnvVar{{Name: "FROM_ACTIVE", Value: "yes"}}, + dst.Spec.Wandb.LegacyOverrides["api"].Env) +} + +func TestConvertRoundTrip_LegacyOverridesIdempotent(t *testing.T) { + withConversionManifestApps(t, "api") + values := withVersion(map[string]interface{}{ + "global": map[string]interface{}{ + "size": "medium", + "env": map[string]interface{}{"B": "2", "A": "1"}, + "extraEnv": map[string]interface{}{ + "C": true, + }, + }, + "api": map[string]interface{}{ + "env": map[string]interface{}{ + "KEY": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{"name": "s", "key": "k"}, + }, + }, + }, + "sizing": map[string]interface{}{ + "medium": map[string]interface{}{ + "resources": map[string]interface{}{ + "requests": map[string]interface{}{"cpu": "2"}, + }, + }, + }, + }, + }) + + first := &appsv2.WeightsAndBiases{} + require.NoError(t, newV1(values).ConvertTo(first)) + + bounced := &WeightsAndBiases{} + require.NoError(t, bounced.ConvertFrom(first)) + + second := &appsv2.WeightsAndBiases{} + require.NoError(t, bounced.ConvertTo(second)) + + require.Equal(t, first.Spec.Wandb.LegacyOverrides, second.Spec.Wandb.LegacyOverrides) + require.Contains(t, first.Spec.Wandb.LegacyOverrides, "api") +} diff --git a/api/v1/weightsandbiases_conversion_test.go b/api/v1/weightsandbiases_conversion_test.go new file mode 100644 index 00000000..7d4ef679 --- /dev/null +++ b/api/v1/weightsandbiases_conversion_test.go @@ -0,0 +1,1803 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + appsv2 "github.com/wandb/operator/api/v2" +) + +// withConversionReader installs a fake reader for the duration of the test +// (cleaned up via t.Cleanup) so tests don't leak the package-level state. +func withConversionReader(t *testing.T, secrets ...*corev1.Secret) { + t.Helper() + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + builder := fake.NewClientBuilder().WithScheme(scheme) + for _, s := range secrets { + builder = builder.WithObjects(s) + } + SetConversionReader(builder.Build()) + t.Cleanup(func() { SetConversionReader(nil) }) +} + +// activeSpecSecret builds a `<cr-name>-spec-active`-shaped Secret with the +// given values map JSON-encoded into data.values. +func activeSpecSecret(t *testing.T, namespace, crName string, values map[string]interface{}) *corev1.Secret { + t.Helper() + raw, err := json.Marshal(values) + require.NoError(t, err) + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: crName + "-spec-active", + Namespace: namespace, + }, + Data: map[string][]byte{"values": raw}, + } +} + +func newV1(values map[string]interface{}) *WeightsAndBiases { + return &WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "default", + }, + Spec: WeightsAndBiasesSpec{ + Values: Object{Object: values}, + }, + } +} + +func TestConvertTo_EmptyValues(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + require.NoError(t, newV1(nil).ConvertTo(dst)) + require.Equal(t, "wandb", dst.Name) + require.Empty(t, dst.Spec.Wandb.Hostname) + require.Empty(t, dst.Spec.Wandb.License) + require.Empty(t, string(dst.Spec.Size)) + require.NotContains(t, dst.Annotations, OIDCPendingAnnotation) +} + +func TestConvertTo_NoGlobalKey(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "mysql": map[string]interface{}{"install": true}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Empty(t, dst.Spec.Wandb.Hostname) + require.Empty(t, string(dst.Spec.Size)) +} + +func TestConvertTo_HostnameAndLicense(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "host": "http://wandb.localhost", + "license": "jwt-token-here", + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "http://wandb.localhost", dst.Spec.Wandb.Hostname) + require.Equal(t, "jwt-token-here", dst.Spec.Wandb.License) + require.NotContains(t, dst.Annotations, OIDCPendingAnnotation) +} + +func TestConvertTo_AllValidSizes(t *testing.T) { + cases := []appsv2.Size{ + appsv2.SizeDev, + appsv2.SizeMicro, + appsv2.SizeSmall, + appsv2.SizeMedium, + appsv2.SizeLarge, + appsv2.SizeXLarge, + appsv2.SizeXXLarge, + } + for _, size := range cases { + t.Run(string(size), func(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"size": string(size)}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, size, dst.Spec.Size) + }) + } +} + +func TestConvertTo_SizeEmptyString(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"size": ""}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Empty(t, string(dst.Spec.Size)) +} + +func TestConvertTo_SizeUnrecognized(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"size": "testing"}, + }) + err := src.ConvertTo(dst) + require.Error(t, err) + require.Contains(t, err.Error(), `"testing"`) +} + +func TestConvertTo_CustomCACerts(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "customCACerts": []interface{}{"---cert-one---", "---cert-two---"}, + "caCertsConfigMap": "corp-ca-certs", + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.Equal(t, []string{"---cert-one---", "---cert-two---"}, dst.Spec.Global.CustomCACerts) + require.Equal(t, "corp-ca-certs", dst.Spec.Global.CACertsConfigMap) +} + +func TestConvertTo_VersionFromAppImageTag(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "app": map[string]interface{}{ + "image": map[string]interface{}{"tag": "0.80.1"}, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "0.80.1", dst.Spec.Wandb.Version) +} + +func TestConvertTo_VersionFallsBackToApiImageTag(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "api": map[string]interface{}{ + "image": map[string]interface{}{"tag": "0.79.2"}, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "0.79.2", dst.Spec.Wandb.Version) +} + +func TestConvertTo_VersionAppWinsOverApi(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "app": map[string]interface{}{ + "image": map[string]interface{}{"tag": "0.80.1"}, + }, + "api": map[string]interface{}{ + "image": map[string]interface{}{"tag": "0.79.2"}, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "0.80.1", dst.Spec.Wandb.Version, "app.image.tag takes precedence over api.image.tag") +} + +func TestConvertTo_VersionEmptyAppFallsBackToApi(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "app": map[string]interface{}{ + "image": map[string]interface{}{"tag": ""}, + }, + "api": map[string]interface{}{ + "image": map[string]interface{}{"tag": "0.79.2"}, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "0.79.2", dst.Spec.Wandb.Version, "empty app tag should not consume the slot; api fallback applies") +} + +func TestConvertTo_VersionAbsent(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"host": "http://x"}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Empty(t, dst.Spec.Wandb.Version) +} + +func TestConvertTo_VersionWithoutGlobal(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "app": map[string]interface{}{ + "image": map[string]interface{}{"tag": "0.80.1"}, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "0.80.1", dst.Spec.Wandb.Version, + "version mapping should run even when global is absent") +} + +func TestConvertTo_ServiceAccountAnnotationsFromApp(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "app": map[string]interface{}{ + "serviceAccount": map[string]interface{}{ + "annotations": map[string]interface{}{ + "iam.gke.io/gcp-service-account": "wandb-app@project.iam.gserviceaccount.com", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, map[string]string{ + "iam.gke.io/gcp-service-account": "wandb-app@project.iam.gserviceaccount.com", + }, dst.Spec.Wandb.ServiceAccount.Annotations) +} + +func TestConvertTo_ServiceAccountAnnotationsFallsBackToApi(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "api": map[string]interface{}{ + "serviceAccount": map[string]interface{}{ + "annotations": map[string]interface{}{ + "iam.gke.io/gcp-service-account": "wandb-api@project.iam.gserviceaccount.com", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "wandb-api@project.iam.gserviceaccount.com", + dst.Spec.Wandb.ServiceAccount.Annotations["iam.gke.io/gcp-service-account"]) +} + +func TestConvertTo_ServiceAccountAnnotationsAppWinsOverApi(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "app": map[string]interface{}{ + "serviceAccount": map[string]interface{}{ + "annotations": map[string]interface{}{ + "iam.gke.io/gcp-service-account": "from-app", + }, + }, + }, + "api": map[string]interface{}{ + "serviceAccount": map[string]interface{}{ + "annotations": map[string]interface{}{ + "iam.gke.io/gcp-service-account": "from-api", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "from-app", + dst.Spec.Wandb.ServiceAccount.Annotations["iam.gke.io/gcp-service-account"], + "app annotations should take precedence over api when both present") +} + +func TestConvertTo_ServiceAccountAnnotationsEmptyAppFallsBackToApi(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "app": map[string]interface{}{ + "serviceAccount": map[string]interface{}{ + "annotations": map[string]interface{}{}, + }, + }, + "api": map[string]interface{}{ + "serviceAccount": map[string]interface{}{ + "annotations": map[string]interface{}{ + "iam.gke.io/gcp-service-account": "from-api", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "from-api", + dst.Spec.Wandb.ServiceAccount.Annotations["iam.gke.io/gcp-service-account"], + "empty app annotations should not consume the slot; api fallback applies") +} + +func TestConvertTo_ServiceAccountAnnotationsAbsent(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"host": "http://x"}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Empty(t, dst.Spec.Wandb.ServiceAccount.Annotations) +} + +func TestConvertTo_ServiceAccountAnnotationsPreservedMultipleKeys(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "app": map[string]interface{}{ + "serviceAccount": map[string]interface{}{ + "annotations": map[string]interface{}{ + "iam.gke.io/gcp-service-account": "wandb-app@p.iam.gserviceaccount.com", + "eks.amazonaws.com/role-arn": "arn:aws:iam::123:role/wandb", + "azure.workload.identity/client-id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Len(t, dst.Spec.Wandb.ServiceAccount.Annotations, 3) + require.Equal(t, "wandb-app@p.iam.gserviceaccount.com", + dst.Spec.Wandb.ServiceAccount.Annotations["iam.gke.io/gcp-service-account"]) + require.Equal(t, "arn:aws:iam::123:role/wandb", + dst.Spec.Wandb.ServiceAccount.Annotations["eks.amazonaws.com/role-arn"]) + require.Equal(t, "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + dst.Spec.Wandb.ServiceAccount.Annotations["azure.workload.identity/client-id"]) +} + +func TestConvertTo_InternalJWTIssuerFromGlobal(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "internalJWTMap": []interface{}{ + map[string]interface{}{ + "issuer": "https://gke-issuer.example.com", + "subject": "system:serviceaccount:default:wandb-weave-trace", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "https://gke-issuer.example.com", dst.Spec.Wandb.InternalServiceAuth.OIDCIssuer) +} + +func TestConvertTo_InternalJWTIssuerFallsBackToApp(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "app": map[string]interface{}{ + "internalJWTMap": []interface{}{ + map[string]interface{}{ + "issuer": "https://app-issuer.example.com", + "subject": "system:serviceaccount:default:wandb-app", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "https://app-issuer.example.com", dst.Spec.Wandb.InternalServiceAuth.OIDCIssuer) +} + +func TestConvertTo_InternalJWTIssuerGlobalWinsOverApp(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "internalJWTMap": []interface{}{ + map[string]interface{}{"issuer": "from-global"}, + }, + }, + "app": map[string]interface{}{ + "internalJWTMap": []interface{}{ + map[string]interface{}{"issuer": "from-app"}, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "from-global", dst.Spec.Wandb.InternalServiceAuth.OIDCIssuer) +} + +func TestConvertTo_InternalJWTIssuerFirstEntryWins(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "internalJWTMap": []interface{}{ + map[string]interface{}{"issuer": "first-issuer"}, + map[string]interface{}{"issuer": "second-issuer"}, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "first-issuer", dst.Spec.Wandb.InternalServiceAuth.OIDCIssuer) +} + +func TestConvertTo_InternalJWTIssuerEmptyGlobalFallsBackToApp(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "internalJWTMap": []interface{}{}, + }, + "app": map[string]interface{}{ + "internalJWTMap": []interface{}{ + map[string]interface{}{"issuer": "from-app"}, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "from-app", dst.Spec.Wandb.InternalServiceAuth.OIDCIssuer) +} + +func TestConvertTo_InternalJWTIssuerAbsent(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"host": "http://x"}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Empty(t, dst.Spec.Wandb.InternalServiceAuth.OIDCIssuer) +} + +func TestConvertTo_IngressEnabledByChartDefaults(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"host": "http://x"}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, appsv2.NetworkingModeIngress, dst.Spec.Networking.Mode, + "ingress.install and ingress.create both default to true; mode should be ingress") +} + +func TestConvertTo_IngressDisabledByInstallFalse(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "ingress": map[string]interface{}{"install": false}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Empty(t, string(dst.Spec.Networking.Mode)) +} + +func TestConvertTo_IngressDisabledByCreateFalse(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "ingress": map[string]interface{}{"create": false}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Empty(t, string(dst.Spec.Networking.Mode)) +} + +func TestConvertTo_IngressClassMaps(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "ingress": map[string]interface{}{"class": "nginx"}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotNil(t, dst.Spec.Networking.Ingress) + require.NotNil(t, dst.Spec.Networking.Ingress.IngressClassName) + require.Equal(t, "nginx", *dst.Spec.Networking.Ingress.IngressClassName) +} + +func TestConvertTo_IngressAnnotationsMap(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "ingress": map[string]interface{}{ + "annotations": map[string]interface{}{ + "kubernetes.io/ingress.class": "gce", + "ingress.gcp.kubernetes.io/pre-shared-cert": "wandb-cert", + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "gce", dst.Spec.Networking.Annotations["kubernetes.io/ingress.class"]) + require.Equal(t, "wandb-cert", + dst.Spec.Networking.Annotations["ingress.gcp.kubernetes.io/pre-shared-cert"]) +} + +func TestConvertTo_IngressAdditionalHostsMap(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "ingress": map[string]interface{}{ + "additionalHosts": []interface{}{"alt1.example.com", "alt2.example.com"}, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, []string{"alt1.example.com", "alt2.example.com"}, dst.Spec.Wandb.AdditionalHostnames) +} + +func TestConvertTo_IngressTLSFirstEntryWins(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "ingress": map[string]interface{}{ + "tls": []interface{}{ + map[string]interface{}{ + "secretName": "wandb-tls", + "hosts": []interface{}{"wandb.example.com"}, + }, + map[string]interface{}{ + "secretName": "wandb-tls-secondary", + "hosts": []interface{}{"alt.example.com"}, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotNil(t, dst.Spec.Networking.TLS) + require.Equal(t, "wandb-tls", dst.Spec.Networking.TLS.SecretName, + "v2 carries only the first TLS entry's secretName") +} + +func TestConvertTo_IngressNameOverrideMaps(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "ingress": map[string]interface{}{"nameOverride": "dpanzella-test-gcp"}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotNil(t, dst.Spec.Networking.Ingress) + require.Equal(t, "dpanzella-test-gcp", dst.Spec.Networking.Ingress.Name) +} + +func TestConvertTo_IngressNameOverrideEmpty(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "ingress": map[string]interface{}{"nameOverride": ""}, + }) + require.NoError(t, src.ConvertTo(dst)) + if dst.Spec.Networking.Ingress != nil { + require.Empty(t, dst.Spec.Networking.Ingress.Name) + } +} + +func TestConvertTo_IngressNoModeOverrideWhenSet(t *testing.T) { + dst := &appsv2.WeightsAndBiases{ + Spec: appsv2.WeightsAndBiasesSpec{ + Networking: appsv2.NetworkingSpec{ + Mode: appsv2.NetworkingModeGatewayAPI, + }, + }, + } + src := newV1(map[string]interface{}{ + "ingress": map[string]interface{}{"class": "nginx"}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, appsv2.NetworkingModeGatewayAPI, dst.Spec.Networking.Mode, + "already-set mode should not be overridden by ingress chart defaults") +} + +func TestConvertTo_OIDCAllLiterals(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "auth": map[string]interface{}{ + "oidc": map[string]interface{}{ + "clientId": "abc", + "secret": "shh", + "authMethod": "client_secret_post", + "issuer": "https://example.com", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + raw, ok := dst.Annotations[OIDCPendingAnnotation] + require.True(t, ok, "expected oidc-pending annotation to be set") + + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "abc", decoded["clientId"]) + require.Equal(t, "shh", decoded["secret"]) + require.Equal(t, "client_secret_post", decoded["authMethod"]) + require.Equal(t, "https://example.com", decoded["issuer"]) + require.NotContains(t, decoded, "oidcSecret") + + require.Empty(t, dst.Spec.Wandb.OIDC.ClientId.Name, "no ref-shaped values, so spec.wandb.oidc stays unset") + require.Empty(t, dst.Spec.Wandb.OIDC.ClientSecret.Name) +} + +func TestConvertTo_OIDCLegacyOidcSecret(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "auth": map[string]interface{}{ + "oidc": map[string]interface{}{ + "clientId": "abc", + "secret": "shh", + "oidcSecret": map[string]interface{}{ + "name": "user-oidc-secret", + "secretKey": "MY_KEY", + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.Equal(t, "user-oidc-secret", dst.Spec.Wandb.OIDC.ClientSecret.Name) + require.Equal(t, "MY_KEY", dst.Spec.Wandb.OIDC.ClientSecret.Key) + + raw := dst.Annotations[OIDCPendingAnnotation] + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "abc", decoded["clientId"]) + require.NotContains(t, decoded, "secret", "literal secret must not be stashed when oidcSecret ref took over") + require.NotContains(t, decoded, "oidcSecret") +} + +func TestConvertTo_OIDCLegacyOidcSecretDefaultKey(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "auth": map[string]interface{}{ + "oidc": map[string]interface{}{ + "oidcSecret": map[string]interface{}{ + "name": "user-oidc-secret", + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.Equal(t, "user-oidc-secret", dst.Spec.Wandb.OIDC.ClientSecret.Name) + require.Equal(t, "OIDC_SECRET", dst.Spec.Wandb.OIDC.ClientSecret.Key) +} + +func TestConvertTo_OIDCValueFromRef(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "auth": map[string]interface{}{ + "oidc": map[string]interface{}{ + "clientId": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "oidc-settings", + "key": "clientId", + }, + }, + }, + "secret": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "oidc-settings", + "key": "clientSecret", + }, + }, + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + oidc := dst.Spec.Wandb.OIDC + require.Equal(t, "oidc-settings", oidc.ClientId.Name) + require.Equal(t, "clientId", oidc.ClientId.Key) + require.Equal(t, "oidc-settings", oidc.ClientSecret.Name) + require.Equal(t, "clientSecret", oidc.ClientSecret.Key) + + require.NotContains(t, dst.Annotations, OIDCPendingAnnotation, + "no literals provided, so no annotation should be created") +} + +func TestConvertTo_OIDCMixedLiteralsAndRefs(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "auth": map[string]interface{}{ + "oidc": map[string]interface{}{ + "clientId": "abc", + "authMethod": "client_secret_post", + "secret": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "oidc-secret", + "key": "clientSecret", + }, + }, + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.Equal(t, "oidc-secret", dst.Spec.Wandb.OIDC.ClientSecret.Name) + require.Empty(t, dst.Spec.Wandb.OIDC.ClientId.Name) + + raw := dst.Annotations[OIDCPendingAnnotation] + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "abc", decoded["clientId"]) + require.Equal(t, "client_secret_post", decoded["authMethod"]) + require.NotContains(t, decoded, "secret") +} + +func TestConvertTo_OIDCValueFromWinsOverLegacyOidcSecret(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "auth": map[string]interface{}{ + "oidc": map[string]interface{}{ + "secret": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "valueFrom-secret", + "key": "clientSecret", + }, + }, + }, + "oidcSecret": map[string]interface{}{ + "name": "legacy-secret", + "secretKey": "LEGACY_KEY", + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.Equal(t, "valueFrom-secret", dst.Spec.Wandb.OIDC.ClientSecret.Name, + "secret.valueFrom should win over the legacy oidcSecret block") +} + +func TestConvertTo_OIDCMalformedRefMap(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "auth": map[string]interface{}{ + "oidc": map[string]interface{}{ + "clientId": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "no-key-here", + }, + }, + }, + }, + }, + }, + }) + err := src.ConvertTo(dst) + require.Error(t, err) + require.Contains(t, err.Error(), "clientId") +} + +func TestConvertTo_OIDCAbsent(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "host": "http://wandb.localhost", + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotContains(t, dst.Annotations, OIDCPendingAnnotation) + require.Empty(t, dst.Spec.Wandb.OIDC.ClientId.Name) +} + +func TestConvertTo_MySQLAllLiterals(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "mysql": map[string]interface{}{ + "host": "mysql.example.com", + "port": int64(3306), + "database": "wandb_local", + "user": "wandb", + "password": "shh", + "caCert": "---cert---", + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + raw, ok := dst.Annotations[MySQLPendingAnnotation] + require.True(t, ok, "expected mysql-pending annotation") + + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "mysql.example.com", decoded["host"]) + require.Equal(t, "wandb_local", decoded["database"]) + require.Equal(t, "wandb", decoded["user"]) + require.Equal(t, "shh", decoded["password"]) + require.Equal(t, "---cert---", decoded["caCert"]) + require.NotContains(t, decoded, "passwordSecret") + + require.NotNil(t, dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql, "externalMysql is always allocated; reconciler fills selectors from the annotation") + require.Empty(t, dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql.Host.Name) + require.Empty(t, dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql.Password.Name) +} + +func TestConvertTo_MySQLLegacyPasswordSecret(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "mysql": map[string]interface{}{ + "host": "mysql.example.com", + "password": "shh", + "passwordSecret": map[string]interface{}{ + "name": "mysql-creds", + "rootPasswordKey": "MYSQL_ROOT_PASSWORD", + "passwordKey": "MYSQL_PASSWORD", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql) + require.Equal(t, "mysql-creds", dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql.Password.Name) + require.Equal(t, "MYSQL_PASSWORD", dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql.Password.Key) + + raw := dst.Annotations[MySQLPendingAnnotation] + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "mysql.example.com", decoded["host"]) + require.NotContains(t, decoded, "password", "literal password must not be stashed when passwordSecret took over") + require.NotContains(t, decoded, "passwordSecret") +} + +func TestConvertTo_MySQLLegacyPasswordSecretDefaultKey(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "mysql": map[string]interface{}{ + "passwordSecret": map[string]interface{}{ + "name": "mysql-creds", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql) + require.Equal(t, "mysql-creds", dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql.Password.Name) + require.Equal(t, "MYSQL_PASSWORD", dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql.Password.Key) +} + +func TestConvertTo_MySQLValueFromRef(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "mysql": map[string]interface{}{ + "host": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "mysql-settings", + "key": "endpoint", + }, + }, + }, + "password": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "mysql-secret", + "key": "password", + }, + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql) + conn := dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql + require.Equal(t, "mysql-settings", conn.Host.Name) + require.Equal(t, "endpoint", conn.Host.Key) + require.Equal(t, "mysql-secret", conn.Password.Name) + require.Equal(t, "password", conn.Password.Key) + + require.NotContains(t, dst.Annotations, MySQLPendingAnnotation, + "no literals provided, so no annotation should be created") +} + +func TestConvertTo_MySQLMixedLiteralsAndRefs(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "mysql": map[string]interface{}{ + "host": "mysql.example.com", + "port": int64(3306), + "password": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "mysql-secret", + "key": "password", + }, + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql) + require.Equal(t, "mysql-secret", dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql.Password.Name) + require.Empty(t, dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql.Host.Name) + + raw := dst.Annotations[MySQLPendingAnnotation] + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "mysql.example.com", decoded["host"]) + require.Contains(t, decoded, "port") + require.NotContains(t, decoded, "password") +} + +func TestConvertTo_MySQLValueFromWinsOverPasswordSecret(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "mysql": map[string]interface{}{ + "password": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "valueFrom-secret", + "key": "password", + }, + }, + }, + "passwordSecret": map[string]interface{}{ + "name": "legacy-secret", + "passwordKey": "LEGACY_KEY", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql) + require.Equal(t, "valueFrom-secret", dst.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql.Password.Name, + "password.valueFrom should win over the legacy passwordSecret block") +} + +func TestConvertTo_MySQLMalformedRefMap(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "mysql": map[string]interface{}{ + "host": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "no-key-here", + }, + }, + }, + }, + }, + }) + err := src.ConvertTo(dst) + require.Error(t, err) + require.Contains(t, err.Error(), "host") +} + +func TestConvertTo_MySQLAbsent(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"host": "http://x"}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotContains(t, dst.Annotations, MySQLPendingAnnotation) +} + +func TestConvertTo_MySQLEmptyMap(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "mysql": map[string]interface{}{}, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotContains(t, dst.Annotations, MySQLPendingAnnotation) +} + +func TestConvertTo_RedisAllLiterals(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "host": "redis.example.com", + "port": int64(6379), + "password": "shh", + "external": true, + "caCert": "----BEGIN CERT----", + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + raw, ok := dst.Annotations[RedisPendingAnnotation] + require.True(t, ok, "expected redis-pending annotation") + + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "redis.example.com", decoded["host"]) + require.Equal(t, "shh", decoded["password"]) + require.Equal(t, "----BEGIN CERT----", decoded["caCert"]) + require.NotContains(t, decoded, "external", "fields outside the known v2 mapping must be dropped") + require.NotContains(t, decoded, "secret") + + require.NotNil(t, dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis, "externalRedis is always allocated; reconciler fills selectors from the annotation") + require.Empty(t, dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Host.Name) +} + +func TestConvertTo_RedisLegacySecretRef(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "host": "redis.example.com", + "password": "shh", + "secret": map[string]interface{}{ + "secretName": "redis-creds", + "secretKey": "REDIS_PASSWORD", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis) + require.Equal(t, "redis-creds", dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Password.Name) + require.Equal(t, "REDIS_PASSWORD", dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Password.Key) + + raw := dst.Annotations[RedisPendingAnnotation] + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "redis.example.com", decoded["host"]) + require.NotContains(t, decoded, "password", "literal password must not be stashed when secret ref took over") +} + +func TestConvertTo_RedisLegacySecretRefDefaultKey(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "secret": map[string]interface{}{ + "secretName": "redis-creds", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis) + require.Equal(t, "redis-creds", dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Password.Name) + require.Equal(t, "REDIS_PASSWORD", dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Password.Key) +} + +func TestConvertTo_RedisValueFromRef(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "host": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "redis-settings", + "key": "endpoint", + }, + }, + }, + "password": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "redis-secret", + "key": "password", + }, + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis) + conn := dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis + require.Equal(t, "redis-settings", conn.Host.Name) + require.Equal(t, "endpoint", conn.Host.Key) + require.Equal(t, "redis-secret", conn.Password.Name) + require.Equal(t, "password", conn.Password.Key) + + require.NotContains(t, dst.Annotations, RedisPendingAnnotation, + "no literals provided, so no annotation should be created") +} + +func TestConvertTo_RedisMixedLiteralsAndRefs(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "host": "redis.example.com", + "port": int64(6379), + "password": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "redis-secret", + "key": "password", + }, + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis) + require.Equal(t, "redis-secret", dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Password.Name) + require.Empty(t, dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Host.Name) + + raw := dst.Annotations[RedisPendingAnnotation] + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "redis.example.com", decoded["host"]) + require.Contains(t, decoded, "port") + require.NotContains(t, decoded, "password") +} + +func TestConvertTo_RedisValueFromWinsOverLegacySecret(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "password": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "valueFrom-secret", + "key": "password", + }, + }, + }, + "secret": map[string]interface{}{ + "secretName": "legacy-secret", + "secretKey": "LEGACY_KEY", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis) + require.Equal(t, "valueFrom-secret", dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Password.Name, + "password.valueFrom should win over the legacy secret block") +} + +func TestConvertTo_RedisMalformedRefMap(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "host": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "no-key-here", + }, + }, + }, + }, + }, + }) + err := src.ConvertTo(dst) + require.Error(t, err) + require.Contains(t, err.Error(), "host") +} + +func TestConvertTo_RedisTLSValueFromInParams(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "params": map[string]interface{}{ + "tls": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "redis-tls", + "key": "enabled", + }, + }, + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotNil(t, dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis) + require.Equal(t, "redis-tls", dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Tls.Name) + require.Equal(t, "enabled", dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Tls.Key) +} + +func TestConvertTo_RedisTLSValueFromInParameters(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "parameters": map[string]interface{}{ + "tls": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "redis-tls", + "key": "enabled", + }, + }, + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotNil(t, dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis) + require.Equal(t, "redis-tls", dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Tls.Name) + require.Equal(t, "enabled", dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Tls.Key) +} + +func TestConvertTo_RedisTLSParamsWinsOverParameters(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "params": map[string]interface{}{ + "tls": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "from-params", + "key": "tls", + }, + }, + }, + }, + "parameters": map[string]interface{}{ + "tls": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "from-parameters", + "key": "tls", + }, + }, + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "from-params", dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Tls.Name, + "params should be checked before parameters") +} + +func TestConvertTo_RedisTLSLiteralStashedInAnnotation(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "params": map[string]interface{}{ + "tls": "true", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + raw, ok := dst.Annotations[RedisPendingAnnotation] + require.True(t, ok, "expected redis-pending annotation for the literal tls") + + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "true", decoded["tls"]) + + require.Empty(t, dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Tls.Name, + "literal tls should not be set on the spec; reconciler materializes it") +} + +func TestConvertTo_RedisTLSAbsent(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "host": "redis.example.com", + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.Empty(t, dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Tls.Name) + require.Empty(t, dst.Spec.Redis[appsv2.DefaultInstanceName].ExternalRedis.Tls.Key) +} + +// TestConvertTo_RedisTLSBooleanStashedAsString locks in that a YAML boolean +// tls toggle is stringified before being stashed. Without this the +// reconciler's string-typed payload field fails to decode a JSON boolean. +func TestConvertTo_RedisTLSBooleanStashedAsString(t *testing.T) { + for _, tc := range []struct { + name string + tls interface{} + want string + }{ + {"true", true, "true"}, + {"false", false, "false"}, + } { + t.Run(tc.name, func(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "params": map[string]interface{}{"tls": tc.tls}, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + raw := dst.Annotations[RedisPendingAnnotation] + // The stashed value must be a JSON string, not a JSON boolean. + require.Contains(t, raw, `"tls":"`+tc.want+`"`) + + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, tc.want, decoded["tls"]) + }) + } +} + +// TestConvertTo_RedisNumericPortStashedAsString confirms numeric scalars are +// also stringified at stash time. +func TestConvertTo_RedisNumericPortStashedAsString(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{ + "port": float64(6379), + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + raw := dst.Annotations[RedisPendingAnnotation] + require.Contains(t, raw, `"port":"6379"`) + + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "6379", decoded["port"]) +} + +func TestConvertTo_RedisAbsent(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"host": "http://x"}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotContains(t, dst.Annotations, RedisPendingAnnotation) +} + +func TestConvertTo_RedisEmptyMap(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "redis": map[string]interface{}{}, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotContains(t, dst.Annotations, RedisPendingAnnotation) +} + +func TestConvertTo_BucketSecretRef(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "bucket": map[string]interface{}{ + "secret": map[string]interface{}{ + "secretName": "bucket-creds", + "accessKeyName": "MY_ACCESS", + "secretKeyName": "MY_SECRET", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.ObjectStore[appsv2.DefaultInstanceName].ExternalObjectStore) + ext := dst.Spec.ObjectStore[appsv2.DefaultInstanceName].ExternalObjectStore + require.Equal(t, "bucket-creds", ext.AccessKey.Name) + require.Equal(t, "MY_ACCESS", ext.AccessKey.Key) + require.Equal(t, "bucket-creds", ext.SecretKey.Name) + require.Equal(t, "MY_SECRET", ext.SecretKey.Key) + + require.NotContains(t, dst.Annotations, BucketPendingAnnotation, + "no literals besides the secret block, so no annotation should be created") +} + +func TestConvertTo_BucketSecretRefDefaultKeys(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "bucket": map[string]interface{}{ + "secret": map[string]interface{}{ + "secretName": "bucket-creds", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.ObjectStore[appsv2.DefaultInstanceName].ExternalObjectStore) + ext := dst.Spec.ObjectStore[appsv2.DefaultInstanceName].ExternalObjectStore + require.Equal(t, "ACCESS_KEY", ext.AccessKey.Key) + require.Equal(t, "SECRET_KEY", ext.SecretKey.Key) +} + +func TestConvertTo_BucketSecretRefEmptyName(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "bucket": map[string]interface{}{ + "secret": map[string]interface{}{ + "secretName": "", + }, + "provider": "s3", + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.ObjectStore[appsv2.DefaultInstanceName].ExternalObjectStore, + "externalObjectStore is always allocated; reconciler fills selectors from the annotation") + require.Empty(t, dst.Spec.ObjectStore[appsv2.DefaultInstanceName].ExternalObjectStore.AccessKey.Name, + "empty secretName should not produce an AccessKey selector") + require.Empty(t, dst.Spec.ObjectStore[appsv2.DefaultInstanceName].ExternalObjectStore.SecretKey.Name) + + raw := dst.Annotations[BucketPendingAnnotation] + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.NotContains(t, decoded, "secret", "secret block must be stripped even when its secretName is empty") + require.Equal(t, "s3", decoded["provider"]) +} + +func TestConvertTo_BucketLiteralsOnlyBucket(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "bucket": map[string]interface{}{ + "provider": "s3", + "name": "wandb-bucket", + "region": "us-east-1", + "accessKey": "AKIA...", + "secretKey": "secret", + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotNil(t, dst.Spec.ObjectStore[appsv2.DefaultInstanceName].ExternalObjectStore, + "externalObjectStore is always allocated; literals stay in the annotation") + require.Empty(t, dst.Spec.ObjectStore[appsv2.DefaultInstanceName].ExternalObjectStore.AccessKey.Name) + + raw, ok := dst.Annotations[BucketPendingAnnotation] + require.True(t, ok) + + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "s3", decoded["provider"]) + require.Equal(t, "wandb-bucket", decoded["name"]) + require.Equal(t, "us-east-1", decoded["region"]) + require.Equal(t, "AKIA...", decoded["accessKey"]) + require.Equal(t, "secret", decoded["secretKey"]) +} + +func TestConvertTo_BucketLiteralsOnlyDefaultBucket(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "defaultBucket": map[string]interface{}{ + "provider": "s3", + "name": "wandb-bucket", + "region": "us-east-1", + "kmsKey": "arn:aws:kms:...", + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + raw, ok := dst.Annotations[BucketPendingAnnotation] + require.True(t, ok) + + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "s3", decoded["provider"]) + require.Equal(t, "wandb-bucket", decoded["name"]) + require.Equal(t, "us-east-1", decoded["region"]) + require.Equal(t, "arn:aws:kms:...", decoded["kmsKey"]) +} + +func TestConvertTo_BucketLiteralsMerged_BucketWins(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "bucket": map[string]interface{}{ + "provider": "gcs", + "name": "bucket-name", + }, + "defaultBucket": map[string]interface{}{ + "provider": "s3", + "name": "default-name", + "region": "us-east-1", + "kmsKey": "kms-fallback", + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + raw := dst.Annotations[BucketPendingAnnotation] + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "gcs", decoded["provider"], "bucket overrides defaultBucket") + require.Equal(t, "bucket-name", decoded["name"], "bucket overrides defaultBucket") + require.Equal(t, "us-east-1", decoded["region"], "defaultBucket fills in fields bucket didn't set") + require.Equal(t, "kms-fallback", decoded["kmsKey"]) +} + +func TestConvertTo_BucketEmptyValueDoesNotOverride(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "bucket": map[string]interface{}{ + "provider": "", + }, + "defaultBucket": map[string]interface{}{ + "provider": "s3", + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + raw := dst.Annotations[BucketPendingAnnotation] + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "s3", decoded["provider"], "empty bucket value must not erase the defaultBucket value") +} + +func TestConvertTo_BucketSecretRefAndLiterals(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "bucket": map[string]interface{}{ + "secret": map[string]interface{}{ + "secretName": "bucket-creds", + }, + "provider": "s3", + "name": "wandb-bucket", + }, + "defaultBucket": map[string]interface{}{ + "region": "us-east-1", + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.NotNil(t, dst.Spec.ObjectStore[appsv2.DefaultInstanceName].ExternalObjectStore) + require.Equal(t, "bucket-creds", dst.Spec.ObjectStore[appsv2.DefaultInstanceName].ExternalObjectStore.AccessKey.Name) + + raw := dst.Annotations[BucketPendingAnnotation] + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.NotContains(t, decoded, "secret") + require.Equal(t, "s3", decoded["provider"]) + require.Equal(t, "wandb-bucket", decoded["name"]) + require.Equal(t, "us-east-1", decoded["region"]) +} + +func TestConvertTo_BucketAbsent(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"host": "http://x"}, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotContains(t, dst.Annotations, BucketPendingAnnotation) + require.Nil(t, dst.Spec.ObjectStore[appsv2.DefaultInstanceName].ExternalObjectStore) +} + +func TestConvertTo_BucketEmptyMaps(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "bucket": map[string]interface{}{}, + "defaultBucket": map[string]interface{}{}, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotContains(t, dst.Annotations, BucketPendingAnnotation) + require.Nil(t, dst.Spec.ObjectStore[appsv2.DefaultInstanceName].ExternalObjectStore) +} + +func TestConvertTo_BucketAllEmptyValues(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "bucket": map[string]interface{}{ + "provider": "", + "name": "", + }, + "defaultBucket": map[string]interface{}{ + "region": "", + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + require.NotContains(t, dst.Annotations, BucketPendingAnnotation, + "every value is empty, so the merged annotation should be skipped") +} + +func TestConvertTo_GlobalNotAMap(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": "not-a-map", + }) + require.Error(t, src.ConvertTo(dst)) +} + +func TestConvertTo_PreservesObjectMeta(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(nil) + src.Labels = map[string]string{"app.kubernetes.io/name": "weightsandbiases"} + src.Annotations = map[string]string{"existing": "value"} + require.NoError(t, src.ConvertTo(dst)) + require.Equal(t, "weightsandbiases", dst.Labels["app.kubernetes.io/name"]) + require.Equal(t, "value", dst.Annotations["existing"]) +} + +// TestConvertRoundTrip locks in the v1 → v2 → v1 → v2 lossless round-trip +// behavior. kube-apiserver bounces objects through this sequence during +// admission, so any data ConvertFrom can't recover would be silently dropped +// from the persisted v2 spec on the next pass. +func TestConvertRoundTrip(t *testing.T) { + original := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "host": "http://wandb.localhost", + "mysql": map[string]interface{}{ + "host": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "mysql-creds", + "key": "Host", + }, + }, + }, + }, + }, + }) + original.Spec.Chart.Object = map[string]interface{}{ + "name": "operator-wandb", + "version": "0.37.1", + } + + // First conversion (apiserver applies the v1 manifest). + firstV2 := &appsv2.WeightsAndBiases{} + require.NoError(t, original.ConvertTo(firstV2)) + require.Equal(t, "http://wandb.localhost", firstV2.Spec.Wandb.Hostname) + require.NotNil(t, firstV2.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql) + require.Equal(t, "mysql-creds", firstV2.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql.Host.Name) + + // Apiserver bounces through ConvertFrom internally. + roundTripped := &WeightsAndBiases{} + require.NoError(t, roundTripped.ConvertFrom(firstV2)) + + // And then ConvertTo again. The second v2 must match the first; otherwise + // the round-trip silently erases data. + secondV2 := &appsv2.WeightsAndBiases{} + require.NoError(t, roundTripped.ConvertTo(secondV2)) + + require.Equal(t, firstV2.Spec.Wandb.Hostname, secondV2.Spec.Wandb.Hostname) + require.NotNil(t, secondV2.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql) + require.Equal(t, firstV2.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql.Host, secondV2.Spec.MySQL[appsv2.DefaultInstanceName].ExternalMysql.Host) +} + +func TestConvertFrom_NoAnnotations(t *testing.T) { + dst := &WeightsAndBiases{} + src := &appsv2.WeightsAndBiases{} + require.NoError(t, dst.ConvertFrom(src)) + require.NotNil(t, dst.Spec.Chart.Object) + require.Empty(t, dst.Spec.Chart.Object) + require.NotNil(t, dst.Spec.Values.Object) + require.Empty(t, dst.Spec.Values.Object) +} + +func TestConvertTo_ActiveSpecSecretOverridesCRValues(t *testing.T) { + withConversionReader(t, activeSpecSecret(t, "default", "wandb", map[string]interface{}{ + "global": map[string]interface{}{ + "host": "http://wandb.from-active-spec", + "license": "active-spec-license", + }, + "app": map[string]interface{}{ + "image": map[string]interface{}{"tag": "0.80.5"}, + }, + })) + + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "host": "http://wandb.from-cr", + "license": "cr-license", + }, + }) + dst := &appsv2.WeightsAndBiases{} + require.NoError(t, src.ConvertTo(dst)) + + require.Equal(t, "http://wandb.from-active-spec", dst.Spec.Wandb.Hostname, + "active-spec Secret should override CR values") + require.Equal(t, "active-spec-license", dst.Spec.Wandb.License) + require.Equal(t, "0.80.5", dst.Spec.Wandb.Version, + "version derives from app.image.tag in the active-spec values") +} + +func TestConvertTo_ActiveSpecAbsentFallsBackToCRValues(t *testing.T) { + // Reader is wired up but the active-spec Secret isn't present. + withConversionReader(t) + + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"host": "http://wandb.from-cr"}, + }) + dst := &appsv2.WeightsAndBiases{} + require.NoError(t, src.ConvertTo(dst)) + + require.Equal(t, "http://wandb.from-cr", dst.Spec.Wandb.Hostname) +} + +func TestConvertTo_ActiveSpecMissingValuesKeyFallsBackToCR(t *testing.T) { + withConversionReader(t, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb-spec-active", + Namespace: "default", + }, + // Only chart, no values — should still fall back to CR. + Data: map[string][]byte{"chart": []byte(`{}`)}, + }) + + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"host": "http://wandb.from-cr"}, + }) + dst := &appsv2.WeightsAndBiases{} + require.NoError(t, src.ConvertTo(dst)) + + require.Equal(t, "http://wandb.from-cr", dst.Spec.Wandb.Hostname) +} + +func TestConvertTo_ActiveSpecMalformedJSONErrors(t *testing.T) { + withConversionReader(t, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb-spec-active", + Namespace: "default", + }, + Data: map[string][]byte{"values": []byte("not json")}, + }) + + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"host": "http://x"}, + }) + dst := &appsv2.WeightsAndBiases{} + err := src.ConvertTo(dst) + require.Error(t, err) + require.Contains(t, err.Error(), "wandb-spec-active") +} + +func TestConvertTo_ActiveSpecScopedByCRName(t *testing.T) { + // Active-spec Secret exists but for a different CR name. + withConversionReader(t, activeSpecSecret(t, "default", "some-other-cr", map[string]interface{}{ + "global": map[string]interface{}{"host": "http://wandb.from-other-cr"}, + })) + + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"host": "http://wandb.from-cr"}, + }) + dst := &appsv2.WeightsAndBiases{} + require.NoError(t, src.ConvertTo(dst)) + + require.Equal(t, "http://wandb.from-cr", dst.Spec.Wandb.Hostname, + "active spec lookup must be scoped to this CR's name") +} + +// TestConvertTo_StashedAnnotationsReflectCRNotActiveSpec confirms that the +// round-trip annotations carry the raw v1 CR values (not the merged active +// spec). This preserves lossless round-trips even when the active-spec Secret +// is mutated between consecutive ConvertFrom/ConvertTo bounces. +func TestConvertTo_StashedAnnotationsReflectCRNotActiveSpec(t *testing.T) { + withConversionReader(t, activeSpecSecret(t, "default", "wandb", map[string]interface{}{ + "global": map[string]interface{}{"host": "http://wandb.from-active-spec"}, + })) + + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{"host": "http://wandb.from-cr"}, + }) + dst := &appsv2.WeightsAndBiases{} + require.NoError(t, src.ConvertTo(dst)) + + stashed := dst.Annotations[v1ValuesAnnotation] + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(stashed), &decoded)) + global := decoded["global"].(map[string]interface{}) + require.Equal(t, "http://wandb.from-cr", global["host"], + "stashed annotation must preserve the CR's raw values for round-trip") +} diff --git a/api/v2/application_types.go b/api/v2/application_types.go new file mode 100644 index 00000000..c5cf1d9c --- /dev/null +++ b/api/v2/application_types.go @@ -0,0 +1,137 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1" + "github.com/wandb/operator/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1" + v1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ApplicationSpec defines the desired state of Application. +type ApplicationSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + Kind string `json:"kind,omitempty"` + + // Replicas is the number of desired instances of the application. + // This field is ignored if HpaTemplate is provided. + Replicas *int32 `json:"replicas,omitempty"` + + MetaTemplate metav1.ObjectMeta `json:"metaTemplate,omitempty"` + PodTemplate corev1.PodTemplateSpec `json:"podTemplate,omitempty"` + + // VolumeClaimTemplates are the persistent volume claim templates used when + // Kind is "StatefulSet". They are immutable on a live StatefulSet, so they + // are only applied at creation time. + // +optional + VolumeClaimTemplates []corev1.PersistentVolumeClaim `json:"volumeClaimTemplates,omitempty"` + + // ServiceName is the name of the (typically headless) Service that governs a + // StatefulSet, giving each pod a stable DNS identity + // (<pod>.<serviceName>.<namespace>.svc). Required for clustered StatefulSet + // workloads such as an HA etcd. Ignored for non-StatefulSet kinds. + // +optional + ServiceName string `json:"serviceName,omitempty"` + + ServiceTemplate *corev1.ServiceSpec `json:"serviceTemplate,omitempty"` + IngressTemplate *networkingv1.IngressSpec `json:"ingressTemplate,omitempty"` + HpaTemplate *autoscalingv2.HorizontalPodAutoscalerSpec `json:"hpaTemplate,omitempty"` + PdbTemplate *policyv1.PodDisruptionBudgetSpec `json:"pdbTemplate,omitempty"` + ScaledObjectTemplate *kedav1alpha1.ScaledObjectSpec `json:"scaledObjectTemplate,omitempty"` + Jobs []batchv1.Job `json:"jobs,omitempty"` + CronJobs []batchv1.CronJob `json:"cronJobs,omitempty"` + + // HTTPRouteTemplate is the desired HTTPRoute spec. Nil means no HTTPRoute. + // +optional + HTTPRouteTemplate *HTTPRouteTemplateSpec `json:"httpRouteTemplate,omitempty"` +} + +// HTTPRouteTemplateSpec contains the fields needed to build a Gateway API HTTPRoute. +type HTTPRouteTemplateSpec struct { + ParentRefs []gatewayv1.ParentReference `json:"parentRefs"` + Hostnames []gatewayv1.Hostname `json:"hostnames,omitempty"` + + // Paths are the URL path prefixes to match. Defaults to ["/"] if empty. + // +optional + Paths []string `json:"paths,omitempty"` + + // PathType controls the match type: "Exact" for exact matching, anything else for prefix. + // +optional + PathType string `json:"pathType,omitempty"` + + // ServicePort is the port on the backend service to route traffic to. + // Nil means no port is specified in the backend ref. + // +optional + ServicePort *gatewayv1.PortNumber `json:"servicePort,omitempty"` +} + +// ApplicationStatus defines the observed state of Application. +type ApplicationStatus struct { + Ready bool `json:"ready"` + CronJobStatuses map[string]batchv1.CronJobStatus `json:"cronJobStatuses,omitempty"` + DeploymentStatus *v1.DeploymentStatus `json:"deploymentStatus,omitempty"` + IngressStatus *networkingv1.IngressStatus `json:"ingressStatus,omitempty"` + JobStatuses map[string]batchv1.JobStatus `json:"jobStatuses,omitempty"` + RolloutStatus *v1alpha1.RolloutStatus `json:"rolloutStatus,omitempty"` + StatefulSetStatus *v1.StatefulSetStatus `json:"statefulSetStatus,omitempty"` + ServiceStatus *corev1.ServiceStatus `json:"serviceStatus,omitempty"` + HPAStatus *autoscalingv2.HorizontalPodAutoscalerStatus `json:"hpaStatus,omitempty"` + + // +optional + HTTPRouteStatus *HTTPRouteStatusSummary `json:"httpRouteStatus,omitempty"` +} + +type HTTPRouteStatusSummary struct { + Accepted bool `json:"accepted,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// Application is the Schema for the applications API. +type Application struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ApplicationSpec `json:"spec,omitempty"` + Status ApplicationStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ApplicationList contains a list of Application. +type ApplicationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Application `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Application{}, &ApplicationList{}) +} diff --git a/api/v2/groupversion_info.go b/api/v2/groupversion_info.go new file mode 100644 index 00000000..9269e5ca --- /dev/null +++ b/api/v2/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v2 contains API Schema definitions for the apps v2 API group. +// +kubebuilder:object:generate=true +// +groupName=apps.wandb.com +package v2 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "apps.wandb.com", Version: "v2"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v2/weightsandbiases_conversion.go b/api/v2/weightsandbiases_conversion.go new file mode 100644 index 00000000..cfcd8b5a --- /dev/null +++ b/api/v2/weightsandbiases_conversion.go @@ -0,0 +1,22 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// Hub marks this type as a conversion hub. +func (*WeightsAndBiases) Hub() {} diff --git a/api/v2/weightsandbiases_types.go b/api/v2/weightsandbiases_types.go new file mode 100644 index 00000000..9ecc0274 --- /dev/null +++ b/api/v2/weightsandbiases_types.go @@ -0,0 +1,785 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +//+kubebuilder:object:root=true +//+kubebuilder:storageversion +//+kubebuilder:subresource:status +//+kubebuilder:resource:shortName=wandb +//+kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=`.status.ready` +//+kubebuilder:printcolumn:name="MySQL",type=string,JSONPath=`.status.mysqlStatus.default.state` +//+kubebuilder:printcolumn:name="Redis",type=string,JSONPath=`.status.redisStatus.default.state` +//+kubebuilder:printcolumn:name="Kafka",type=string,JSONPath=`.status.kafkaStatus.state` +//+kubebuilder:printcolumn:name="ObjectStore",type=string,JSONPath=`.status.objectStoreStatus.default.state` +//+kubebuilder:printcolumn:name="ClickHouse",type=string,JSONPath=`.status.clickhouseStatus.default.state` + +// WeightsAndBiases is the Schema for the weightsandbiases API. +type WeightsAndBiases struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec WeightsAndBiasesSpec `json:"spec,omitempty"` + Status WeightsAndBiasesStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// WeightsAndBiasesList contains a list of WeightsAndBiases. +type WeightsAndBiasesList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []WeightsAndBiases `json:"items"` +} + +func init() { + SchemeBuilder.Register(&WeightsAndBiases{}, &WeightsAndBiasesList{}) +} + +// DefaultInstanceName is the reserved map key identifying the fallback instance +// for each multi-instance infrastructure type (MySQL, Redis, ObjectStore, +// ClickHouse). When an application requests an instance that is not provisioned, +// the operator resolves to this instance instead. +const DefaultInstanceName = "default" + +// ResolveInstance returns the entry for key, falling back to the +// DefaultInstanceName entry when key is empty or absent. The boolean reports +// whether a value was found. +func ResolveInstance[T any](m map[string]T, key string) (T, bool) { + if key == "" { + key = DefaultInstanceName + } + if v, ok := m[key]; ok { + return v, true + } + if v, ok := m[DefaultInstanceName]; ok { + return v, true + } + var zero T + return zero, false +} + +type Size string + +const ( + SizeDev Size = "dev" + SizeMicro Size = "micro" + SizeSmall Size = "small" + SizeMedium Size = "medium" + SizeLarge Size = "large" + SizeXLarge Size = "xlarge" + SizeXXLarge Size = "xxlarge" +) + +type OnDeletePolicy string + +const ( + // DetachOnDelete removes ownership of infrastructure CRs so they survive WandB CR deletion + DetachOnDelete OnDeletePolicy = "detach" + // PurgeOnDelete will delete all associated resources upon deletion + PurgeOnDelete OnDeletePolicy = "purge" +) + +type RetentionPolicy struct { + // +kubebuilder:default="detach" + OnDelete OnDeletePolicy `json:"onDelete" default:"detach"` +} + +// WeightsAndBiasesSpec defines the desired state of WeightsAndBiases. +type WeightsAndBiasesSpec struct { + // Size is akin to high-level environment info + // +kubebuilder:validation:Enum=dev;micro;small;medium;large;xlarge;xxlarge + Size Size `json:"size,omitempty"` + // RequireLimits By default, only resource requests are set for deployments, set to true to also set resource limits + RequireLimits bool `json:"requireLimits,omitempty"` + + RetentionPolicy RetentionPolicy `json:"retentionPolicy"` + + // Global holds settings shared across all managed components. + // +optional + Global GlobalSpec `json:"global,omitempty"` + + Wandb WandbAppSpec `json:"wandb,omitempty"` + + Affinity *corev1.Affinity `json:"affinity,omitempty"` + Tolerations *[]corev1.Toleration `json:"tolerations,omitempty"` + + // MySQL, Redis, ObjectStore and ClickHouse are keyed by instance name. The + // reserved DefaultInstanceName key identifies the fallback instance used when + // an application requests an instance that is not provisioned. + MySQL map[string]MySQLSpec `json:"mysql,omitempty"` + Redis map[string]RedisSpec `json:"redis,omitempty"` + Kafka KafkaSpec `json:"kafka,omitempty"` + ObjectStore map[string]ObjectStoreSpec `json:"objectStore,omitempty"` + ClickHouse map[string]ClickHouseSpec `json:"clickhouse,omitempty"` + + // Networking configures how the W&B application is exposed externally. + // +optional + Networking NetworkingSpec `json:"networking,omitempty"` +} + +// GlobalSpec holds settings shared across every managed component. +type GlobalSpec struct { + // ImageRegistry, when set, retargets the container images to this registry. + // Intended for air-gapped installs whose nodes cannot reach public registries; pair it + // with a registry pre-populated by `wsm registry mirror`. + ImageRegistry string `json:"imageRegistry,omitempty"` + + // CustomCACerts contains PEM-encoded CA certificates that should be trusted + // by W&B application workloads. + // +optional + CustomCACerts []string `json:"customCACerts,omitempty"` + + // CACertsConfigMap references a ConfigMap in the W&B namespace whose keys + // contain CA certificates. Keys should use a .crt suffix so standard CA + // update tooling can discover them. + // +optional + CACertsConfigMap string `json:"caCertsConfigMap,omitempty"` +} + +type NetworkingMode string + +const ( + NetworkingModeNone NetworkingMode = "" + NetworkingModeIngress NetworkingMode = "ingress" + NetworkingModeGatewayAPI NetworkingMode = "gateway" +) + +type NetworkingSpec struct { + // Mode selects the networking strategy: "Ingress" or "GatewayAPI". + // Empty/unset means no operator-managed ingress (preserves current NodePort behavior). + // +kubebuilder:validation:Enum="";ingress;gateway + Mode NetworkingMode `json:"mode,omitempty"` + + // +optional + Ingress *IngressConfig `json:"ingress,omitempty"` + + // +optional + GatewayAPI *GatewayAPIConfig `json:"gatewayAPI,omitempty"` + + // +optional + TLS *TLSConfig `json:"tls,omitempty"` + + // Annotations applied to all generated Ingress or Gateway resources. + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +type IngressConfig struct { + // +optional + IngressClassName *string `json:"ingressClassName,omitempty"` + + // Name overrides the generated Ingress resource name. When empty the + // operator defaults to the CR name. + // +optional + Name string `json:"name,omitempty"` +} + +type GatewayAPIConfig struct { + Gateway GatewayConfig `json:"gateway"` + + // ListenerName selects which listener on the Gateway to attach HTTPRoutes to. + // +optional + ListenerName *string `json:"listenerName,omitempty"` +} + +type GatewayConfig struct { + // Managed controls whether the operator creates and manages the Gateway resource. + // When false (default), gatewayRef must reference an existing Gateway. + // +kubebuilder:default=false + Managed bool `json:"managed,omitempty"` + + // +optional + GatewayRef *GatewayReference `json:"gatewayRef,omitempty"` + + // GatewayClassName is required when managed=true. + // +optional + GatewayClassName *string `json:"gatewayClassName,omitempty"` + + // Listeners defines the listeners on a managed Gateway. + // If empty and managed=true, a default HTTPS listener is created from + // spec.wandb.hostname and spec.networking.tls. + // +optional + Listeners []GatewayListener `json:"listeners,omitempty"` + + // Annotations passed to the managed Gateway resource. + // +optional + InfrastructureAnnotations map[string]string `json:"infrastructureAnnotations,omitempty"` +} + +type GatewayReference struct { + Name string `json:"name"` + // +optional + Namespace string `json:"namespace,omitempty"` +} + +type GatewayListener struct { + Name string `json:"name"` + Port int32 `json:"port"` + Protocol string `json:"protocol"` + // +optional + Hostname *string `json:"hostname,omitempty"` + // +optional + TLS *ListenerTLSConfig `json:"tls,omitempty"` +} + +type ListenerTLSConfig struct { + // +optional + Mode *string `json:"mode,omitempty"` + // +optional + CertificateRef *SecretRef `json:"certificateRef,omitempty"` +} + +type SecretRef struct { + Name string `json:"name"` + // +optional + Namespace string `json:"namespace,omitempty"` +} + +type TLSConfig struct { + // +optional + SecretName string `json:"secretName,omitempty"` + // +optional + CertManager *CertManagerConfig `json:"certManager,omitempty"` +} + +type CertManagerConfig struct { + // +optional + ClusterIssuer string `json:"clusterIssuer,omitempty"` + // +optional + Issuer string `json:"issuer,omitempty"` +} + +func (w *WeightsAndBiases) GetRetentionPolicy(spec ManagedInfraSpec) RetentionPolicy { + if spec.RetentionPolicy != nil { + return *spec.RetentionPolicy + } + return w.Spec.RetentionPolicy +} + +func (w *WeightsAndBiases) GetAffinity(spec ManagedInfraSpec) *corev1.Affinity { + if spec.Affinity != nil { + return spec.Affinity + } + return w.Spec.Affinity +} + +func (w *WeightsAndBiases) GetTolerations(spec ManagedInfraSpec) *[]corev1.Toleration { + if spec.Tolerations != nil { + return spec.Tolerations + } + return w.Spec.Tolerations +} + +// ValidMysqlReplicaCount reports whether r is a count Moco accepts: a positive odd number. +func ValidMysqlReplicaCount(r int32) bool { + return r > 0 && r%2 == 1 +} + +// WandbAppSpec defines the configuration for the Wandb application deployment. +type WandbAppSpec struct { + Hostname string `json:"hostname"` + License string `json:"license,omitempty"` + ManifestRepository string `json:"manifestRepository,omitempty"` + Version string `json:"version"` + Features map[string]bool `json:"features"` + InternalServiceAuth InternalServiceAuth `json:"internalServiceAuth,omitempty"` + BucketProxy bool `json:"bucketProxy"` + + ServiceAccount ServiceAccountSpec `json:"serviceAccount,omitempty"` + + // Probes configures default health probes for W&B application workload + // containers generated by the operator. The full Kubernetes Probe shape is + // exposed so operators can tune timings and handlers for their environment. + // Explicit probes on generated containers remain authoritative; these values + // only fill missing probes or missing probe fields. + // +optional + Probes WandbProbeDefaults `json:"probes,omitempty"` + + // +optional + AdditionalHostnames []string `json:"additionalHostnames,omitempty"` + + // +optional + OIDC OidcSpec `json:"oidc,omitempty"` + + // LegacyOverrides holds env/resource overrides extracted from v1 + // spec.values, keyed by manifest application name plus the reserved + // "global" key (env only, applied to every application). Unknown keys are + // logged and ignored. Conversion-owned; prefer first-class fields over + // hand-editing. + // +optional + LegacyOverrides map[string]LegacyOverrides `json:"legacyOverrides,omitempty"` +} + +// LegacyOverridesGlobalKey is the reserved LegacyOverrides key whose env +// applies to every application and migration job. +const LegacyOverridesGlobalKey = "global" + +// DefaultManifestRepository is used when spec.wandb.manifestRepository is +// unset — by the defaulting webhook and by v1 conversion (which runs first). +const DefaultManifestRepository = "oci://us-docker.pkg.dev/wandb-production/public/wandb/server-manifest" + +// LegacyOverrides holds v1-derived overrides for one application (or "global"). +type LegacyOverrides struct { + // Env is applied last, replacing same-named manifest or injected vars. + // +optional + Env []corev1.EnvVar `json:"env,omitempty"` + + // Resources overlays sizing-derived resources per field; limits are still + // gated by spec.requireLimits. + // +optional + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` +} + +type WandbProbeDefaults struct { + // StartupProbe defines default startup probe values for generated W&B + // application containers. If the handler is omitted, the operator derives + // one from the container readiness probe, then liveness probe. + // +optional + StartupProbe *corev1.Probe `json:"startupProbe,omitempty"` + + // LivenessProbe defines default liveness probe values for generated W&B + // application containers. + // +optional + LivenessProbe *corev1.Probe `json:"livenessProbe,omitempty"` + + // ReadinessProbe defines default readiness probe values for generated W&B + // application containers. + // +optional + ReadinessProbe *corev1.Probe `json:"readinessProbe,omitempty"` +} + +type ServiceAccountSpec struct { + // +kubebuilder:default=true + Create *bool `json:"create"` + // +kubebuilder:default="wandb" + ServiceAccountName string `json:"serviceAccountName,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` +} + +type InternalServiceAuth struct { + Enabled *bool `json:"enabled,omitempty"` + OIDCIssuer string `json:"oidcIssuer,omitempty"` +} + +// OidcSpec defines the structure for OpenID Connect (OIDC) configuration used in Wandb application deployments. +type OidcSpec struct { + ClientId corev1.SecretKeySelector `json:"clientId,omitempty"` + ClientSecret corev1.SecretKeySelector `json:"clientSecret,omitempty"` + IssuerUrl corev1.SecretKeySelector `json:"issuerUrl,omitempty"` + AuthMethod corev1.SecretKeySelector `json:"authMethod,omitempty"` + + SessionLength string `json:"sessionLength,omitempty"` +} + +type ManagedInfraSpec struct { + RetentionPolicy *RetentionPolicy `json:"retentionPolicy,omitempty"` + + Affinity *corev1.Affinity `json:"affinity,omitempty"` + Tolerations *[]corev1.Toleration `json:"tolerations,omitempty"` +} + +// MySQLSpec fields have many default values that, if unspecified, +// will be applied by a defaulting webook +type MySQLSpec struct { + ManagedMysql *ManagedMysqlSpec `json:"managedMysql,omitempty"` + ExternalMysql *MysqlConnection `json:"externalMysql,omitempty"` +} + +type ManagedMysqlSpec struct { + ManagedInfraSpec `json:",inline"` + + StorageSize string `json:"storageSize,omitempty"` + Replicas int32 `json:"replicas,omitempty"` + Config MySQLConfig `json:"config,omitempty"` + Namespace string `json:"namespace,omitempty"` + Name string `json:"name,omitempty"` + Telemetry Telemetry `json:"telemetry,omitempty"` +} + +type MysqlConnection struct { + // required + Host corev1.SecretKeySelector `json:"host,omitempty"` + Port corev1.SecretKeySelector `json:"port,omitempty"` + Database corev1.SecretKeySelector `json:"database,omitempty"` + Username corev1.SecretKeySelector `json:"username,omitempty"` + Password corev1.SecretKeySelector `json:"password,omitempty"` + + // optional + Tls corev1.SecretKeySelector `json:"tls,omitempty"` + SslCa corev1.SecretKeySelector `json:"sslCa,omitempty"` + SslCert corev1.SecretKeySelector `json:"sslCert,omitempty"` + SslKey corev1.SecretKeySelector `json:"sslKey,omitempty"` + + // generated by operator + URL corev1.SecretKeySelector `json:"url,omitempty"` +} + +type MySQLConfig struct { + Resources corev1.ResourceRequirements `json:"resources,omitempty"` +} + +// Telemetry defines telemetry configuration for infrastructure components +type Telemetry struct { + // +kubebuilder:default=true + Enabled bool `json:"enabled" default:"true"` +} + +// RedisSpec defines the desired state of the Redis infrastructure component. +type RedisSpec struct { + ManagedRedis *ManagedRedisSpec `json:"managedRedis,omitempty"` + ExternalRedis *RedisConnection `json:"externalRedis,omitempty"` +} + +type ManagedRedisSpec struct { + ManagedInfraSpec `json:",inline"` + + StorageSize string `json:"storageSize,omitempty"` + Config RedisConfig `json:"config,omitempty"` + Sentinel RedisSentinelSpec `json:"sentinel,omitempty"` + Namespace string `json:"namespace,omitempty"` + Name string `json:"name,omitempty"` + Telemetry Telemetry `json:"telemetry,omitempty"` +} + +type RedisConnection struct { + Host corev1.SecretKeySelector `json:"host,omitempty"` + Port corev1.SecretKeySelector `json:"port,omitempty"` + Password corev1.SecretKeySelector `json:"password,omitempty"` + Tls corev1.SecretKeySelector `json:"tls,omitempty"` + SslCa corev1.SecretKeySelector `json:"sslCa,omitempty"` + + URL corev1.SecretKeySelector `json:"url,omitempty"` +} + +type RedisConfig struct { + Resources corev1.ResourceRequirements `json:"resources,omitempty"` +} + +type RedisSentinelSpec struct { + Enabled bool `json:"enabled"` + Config RedisSentinelConfig `json:"config,omitempty"` +} + +type RedisSentinelConfig struct { + MasterName string `json:"masterName,omitempty"` + Resources corev1.ResourceRequirements `json:"resources,omitempty"` +} + +// KafkaSpec defines the desired state of the Kafka infrastructure component. +// Kafka is managed-only (backed by Bufstream); there is no external Kafka option. +type KafkaSpec struct { + ManagedKafka *ManagedKafkaSpec `json:"managedKafka,omitempty"` +} + +type ManagedKafkaSpec struct { + ManagedInfraSpec `json:",inline"` + + StorageSize string `json:"storageSize,omitempty"` + Replicas int32 `json:"replicas,omitempty"` + Config KafkaConfig `json:"config,omitempty"` + Namespace string `json:"namespace,omitempty"` + Name string `json:"name,omitempty"` + Telemetry Telemetry `json:"telemetry,omitempty"` + SkipDataRecovery bool `json:"skipDataRecovery,omitempty"` +} + +type KafkaConnection struct { + Host corev1.SecretKeySelector `json:"host,omitempty"` + Port corev1.SecretKeySelector `json:"port,omitempty"` + BrokerEndpoint corev1.SecretKeySelector `json:"brokerEndpoint,omitempty"` + ClusterID corev1.SecretKeySelector `json:"clusterID,omitempty"` + + URL corev1.SecretKeySelector `json:"url,omitempty"` +} + +type KafkaConfig struct { + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + ReplicationConfig KafkaReplicationConfig `json:"replicationConfig,omitempty"` +} + +type KafkaReplicationConfig struct { + DefaultReplicationFactor int32 `json:"defaultReplicationFactor,omitempty"` + MinInSyncReplicas int32 `json:"minInSyncReplicas,omitempty"` + OffsetsTopicRF int32 `json:"offsetsTopicRF,omitempty"` + TransactionStateRF int32 `json:"transactionStateISR,omitempty"` + TransactionStateISR int32 `json:"transactionStateRF,omitempty"` +} + +// ObjectStoreSpec defines the desired state of the object store infrastructure component. +type ObjectStoreSpec struct { + ManagedObjectStore *ManagedObjectStoreSpec `json:"managedObjectStore,omitempty"` + ExternalObjectStore *ObjectStoreConnection `json:"externalObjectStore,omitempty"` +} + +type ManagedObjectStoreSpec struct { + ManagedInfraSpec `json:",inline"` + SeaweedObjectStoreSpec SeaweedObjectStoreSpec `json:"SeaweedObjectStoreSpec,omitempty"` + StorageSize string `json:"storageSize,omitempty"` + Replicas int32 `json:"replicas,omitempty"` + Copies int32 `json:"copies,omitempty"` + Config ObjectStoreConfig `json:"config,omitempty"` + Namespace string `json:"namespace,omitempty"` + Name string `json:"name,omitempty"` + Telemetry Telemetry `json:"telemetry,omitempty"` +} + +type SeaweedObjectStoreSpec struct { + TlsEnabled bool `json:"tlsEnabled,omitempty"` + // FilerStorageSize sizes the filer's metadata index disk. It grows with the + // number of objects, not their total size, so bump it for large object counts. + // Defaults to 20Gi when unset. + FilerStorageSize string `json:"filerStorageSize,omitempty"` +} + +// ObjectStoreProvider selects the object store backend for an external object store. +type ObjectStoreProvider string + +const ( + ObjectStoreProviderS3 ObjectStoreProvider = "s3" + ObjectStoreProviderGCS ObjectStoreProvider = "gcs" + ObjectStoreProviderAzure ObjectStoreProvider = "azure" +) + +type ObjectStoreConnection struct { + // Provider selects the externalObjectStore backend (s3, gcs, or azure) from a secret key; defaults to s3 when absent. + Provider corev1.SecretKeySelector `json:"provider,omitempty"` + + Endpoint corev1.SecretKeySelector `json:"endpoint,omitempty"` + Port corev1.SecretKeySelector `json:"port,omitempty"` + AccessKey corev1.SecretKeySelector `json:"accessKey,omitempty"` + SecretKey corev1.SecretKeySelector `json:"secretKey,omitempty"` + Bucket corev1.SecretKeySelector `json:"bucket,omitempty"` + // Path is an optional key prefix within the bucket under which W&B stores its data. + Path corev1.SecretKeySelector `json:"path,omitempty"` + Region corev1.SecretKeySelector `json:"region,omitempty"` + TlsEnabled corev1.SecretKeySelector `json:"tlsEnabled,omitempty"` + ForcePathStyle corev1.SecretKeySelector `json:"forcePathStyle,omitempty"` + URL corev1.SecretKeySelector `json:"url,omitempty"` +} + +type ObjectStoreConfig struct { + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + AccessKey string `json:"accessKey,omitempty"` + + // Deprecated: Use AccessKey instead. Kept for backward compatibility during migration. + RootUser string `json:"rootUser,omitempty"` + // Deprecated: No longer used. Kept to avoid schema validation failures on upgrade. + MinioBrowserSetting string `json:"minioBrowserSetting,omitempty"` +} + +// ClickHouseSpec defines the desired state of the ClickHouse infrastructure component. +type ClickHouseSpec struct { + ManagedClickHouse *ManagedClickHouseSpec `json:"managedClickhouse,omitempty"` + ExternalClickHouse *ClickHouseConnection `json:"externalClickhouse,omitempty"` +} + +type ManagedClickHouseSpec struct { + ManagedInfraSpec `json:",inline"` + + StorageSize string `json:"storageSize,omitempty"` + Replicas int32 `json:"replicas,omitempty"` + Version string `json:"version,omitempty"` + Config ClickHouseConfig `json:"config,omitempty"` + Namespace string `json:"namespace,omitempty"` + Name string `json:"name,omitempty"` + Telemetry Telemetry `json:"telemetry,omitempty"` + + // ObjectStorage configures the S3-backed disk that holds ClickHouse table + // data in the configured W&B object store (managed SeaweedFS or external + // bucket). Managed ClickHouse always stores table data in object storage; + // StorageSize sizes the local PV used only for metadata, system tables, and + // the S3 read cache. + ObjectStorage ClickHouseObjectStorageSpec `json:"objectStorage,omitempty"` + + // Keeper configures the ClickHouse Keeper ensemble that coordinates + // ReplicatedMergeTree replication across ClickHouse replicas. + Keeper ClickHouseKeeperSpec `json:"keeper,omitempty"` +} + +// ClickHouseObjectStorageSpec configures object-store-backed storage for managed +// ClickHouse. +type ClickHouseObjectStorageSpec struct { + // Prefix is the key prefix within the bucket under which ClickHouse stores + // its data. Lets multiple consumers share a single bucket. Defaults to + // "clickhouse/". + Prefix string `json:"prefix,omitempty"` + + // Insecure connects to the object store over HTTP instead of HTTPS. It only + // applies to external object stores that do not advertise a scheme; the + // managed object store's scheme is taken from its connection. Defaults to + // false (HTTPS). + Insecure bool `json:"insecure,omitempty"` +} + +// ClickHouseKeeperSpec configures the managed ClickHouse Keeper ensemble. +type ClickHouseKeeperSpec struct { + // Replicas is the number of Keeper nodes. Use an odd number (1, 3, 5) so the + // ensemble can form a quorum. Defaults to 3. + Replicas int32 `json:"replicas,omitempty"` + + // StorageSize is the persistent volume size for each Keeper node's raft log + // and snapshots. Keeper state is small; defaults to a modest value. + StorageSize string `json:"storageSize,omitempty"` + + // Config holds resource requirements for the Keeper pods. + Config ClickHouseConfig `json:"config,omitempty"` +} + +type ClickHouseConnection struct { + Host corev1.SecretKeySelector `json:"host,omitempty"` + TCPPort corev1.SecretKeySelector `json:"tcpPort,omitempty"` + HTTPPort corev1.SecretKeySelector `json:"httpPort,omitempty"` + Database corev1.SecretKeySelector `json:"database,omitempty"` + Username corev1.SecretKeySelector `json:"username,omitempty"` + Password corev1.SecretKeySelector `json:"password,omitempty"` + + URL corev1.SecretKeySelector `json:"url,omitempty"` +} + +type ClickHouseConfig struct { + Resources corev1.ResourceRequirements `json:"resources,omitempty"` +} + +// WeightsAndBiasesStatus defines the observed state of WeightsAndBiases. +type WeightsAndBiasesStatus struct { + Ready bool `json:"ready"` + Wandb WandbStatus `json:"wandb,omitempty"` + // MySQLStatus, RedisStatus, ObjectStoreStatus and ClickHouseStatus are keyed + // by instance name, mirroring the corresponding spec maps. + MySQLStatus map[string]MysqlInfraStatus `json:"mysqlStatus,omitempty"` + RedisStatus map[string]RedisInfraStatus `json:"redisStatus,omitempty"` + KafkaStatus KafkaInfraStatus `json:"kafkaStatus,omitempty"` + ObjectStoreStatus map[string]ObjectStoreInfraStatus `json:"objectStoreStatus,omitempty"` + ClickHouseStatus map[string]ClickHouseInfraStatus `json:"clickhouseStatus,omitempty"` + TelemetryStatus TelemetryInfraStatus `json:"telemetryStatus,omitempty"` + // GeneratedSecrets stores references to secrets generated by the operator + // from the server manifest's generatedSecrets section. The key is the + // logical secret name from the manifest, and the value is a SecretKeySelector + // referencing the concrete Secret and key that holds the generated value. + GeneratedSecrets map[string]corev1.SecretKeySelector `json:"generatedSecrets,omitempty"` + ObservedGeneration int64 `json:"observedGeneration"` + + // +optional + GatewayStatus *GatewayStatusSummary `json:"gatewayStatus,omitempty"` + // +optional + IngressStatus *IngressStatusSummary `json:"ingressStatus,omitempty"` +} + +type GatewayStatusSummary struct { + Name string `json:"name,omitempty"` + Ready bool `json:"ready,omitempty"` + Addresses []string `json:"addresses,omitempty"` + GatewayRef *GatewayReference `json:"gatewayRef,omitempty"` +} + +type IngressStatusSummary struct { + Name string `json:"name,omitempty"` + LoadBalancerIngress []corev1.LoadBalancerIngress `json:"loadBalancerIngress,omitempty"` +} + +type WandbStatus struct { + Hostname string `json:"hostname"` + + // +kubebuilder:default:={} + Applications map[string]ApplicationStatus `json:"applications,omitempty"` + + Migration WandbMigrationStatus `json:"migration,omitempty"` + + // MySQLInit tracks the per-instance database-initialization job, keyed by + // managed MySQL instance name. + // +kubebuilder:default:={} + MySQLInit map[string]MigrationJobStatus `json:"mysqlInit,omitempty"` +} + +type WandbMigrationStatus struct { + Version string `json:"version,omitempty"` + LastSuccessVersion string `json:"lastSuccessVersion,omitempty"` + Ready bool `json:"ready,omitempty"` + Reason string `json:"reason,omitempty"` + Jobs map[string]MigrationJobStatus `json:"jobs,omitempty"` +} + +type MigrationJobStatus struct { + Name string `json:"name,omitempty"` + Succeeded bool `json:"succeeded,omitempty"` + Failed bool `json:"failed,omitempty"` + Message string `json:"message,omitempty"` +} + +type WBInfraStatus struct { + Ready bool `json:"ready"` + State string `json:"state,omitempty" default:"Unknown"` + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +type MysqlInfraStatus struct { + WBInfraStatus `json:",inline"` + Connection MysqlConnection `json:"connection,omitempty"` +} + +type RedisInfraStatus struct { + WBInfraStatus `json:",inline"` + Connection RedisConnection `json:"connection,omitempty"` +} + +type KafkaInfraStatus struct { + WBInfraStatus `json:",inline"` + Connection KafkaConnection `json:"connection,omitempty"` +} + +type ObjectStoreInfraStatus struct { + WBInfraStatus `json:",inline"` + Connection ObjectStoreConnection `json:"connection,omitempty"` +} + +type ClickHouseInfraStatus struct { + WBInfraStatus `json:",inline"` + Connection ClickHouseConnection `json:"connection,omitempty"` +} + +type TelemetryInfraStatus struct { + WBInfraStatus `json:",inline"` + Mode string `json:"mode,omitempty"` + Connection TelemetryConnectionStatus `json:"connection,omitempty"` +} + +type TelemetryConnectionStatus struct { + ManagedNamespace string `json:"managedNamespace,omitempty"` + ConnectionSecret string `json:"connectionSecret,omitempty"` + Protocol string `json:"protocol,omitempty"` + MetricsExporter string `json:"metricsExporter,omitempty"` + LogsExporter string `json:"logsExporter,omitempty"` + TracesExporter string `json:"tracesExporter,omitempty"` + MetricsEndpoint string `json:"metricsEndpoint,omitempty"` + LogsEndpoint string `json:"logsEndpoint,omitempty"` + TracesEndpoint string `json:"tracesEndpoint,omitempty"` + ServiceName string `json:"serviceName,omitempty"` + ResourceAttributes string `json:"resourceAttributes,omitempty"` + GorillaTracer string `json:"gorillaTracer,omitempty"` + StatsdAddress string `json:"statsdAddress,omitempty"` + DatadogTraceAgentURL string `json:"datadogTraceAgentURL,omitempty"` + DatadogTraceAgentHost string `json:"datadogTraceAgentHost,omitempty"` + DatadogTraceAgentPort string `json:"datadogTraceAgentPort,omitempty"` +} diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go new file mode 100644 index 00000000..30864877 --- /dev/null +++ b/api/v2/zz_generated.deepcopy.go @@ -0,0 +1,1703 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v2 + +import ( + "github.com/kedacore/keda/v2/apis/keda/v1alpha1" + argoproj_io_rolloutsv1alpha1 "github.com/wandb/operator/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" + "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + apisv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Application) DeepCopyInto(out *Application) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Application. +func (in *Application) DeepCopy() *Application { + if in == nil { + return nil + } + out := new(Application) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Application) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationList) DeepCopyInto(out *ApplicationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Application, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationList. +func (in *ApplicationList) DeepCopy() *ApplicationList { + if in == nil { + return nil + } + out := new(ApplicationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ApplicationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + in.MetaTemplate.DeepCopyInto(&out.MetaTemplate) + in.PodTemplate.DeepCopyInto(&out.PodTemplate) + if in.VolumeClaimTemplates != nil { + in, out := &in.VolumeClaimTemplates, &out.VolumeClaimTemplates + *out = make([]v1.PersistentVolumeClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ServiceTemplate != nil { + in, out := &in.ServiceTemplate, &out.ServiceTemplate + *out = new(v1.ServiceSpec) + (*in).DeepCopyInto(*out) + } + if in.IngressTemplate != nil { + in, out := &in.IngressTemplate, &out.IngressTemplate + *out = new(networkingv1.IngressSpec) + (*in).DeepCopyInto(*out) + } + if in.HpaTemplate != nil { + in, out := &in.HpaTemplate, &out.HpaTemplate + *out = new(autoscalingv2.HorizontalPodAutoscalerSpec) + (*in).DeepCopyInto(*out) + } + if in.PdbTemplate != nil { + in, out := &in.PdbTemplate, &out.PdbTemplate + *out = new(policyv1.PodDisruptionBudgetSpec) + (*in).DeepCopyInto(*out) + } + if in.ScaledObjectTemplate != nil { + in, out := &in.ScaledObjectTemplate, &out.ScaledObjectTemplate + *out = new(v1alpha1.ScaledObjectSpec) + (*in).DeepCopyInto(*out) + } + if in.Jobs != nil { + in, out := &in.Jobs, &out.Jobs + *out = make([]batchv1.Job, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.CronJobs != nil { + in, out := &in.CronJobs, &out.CronJobs + *out = make([]batchv1.CronJob, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.HTTPRouteTemplate != nil { + in, out := &in.HTTPRouteTemplate, &out.HTTPRouteTemplate + *out = new(HTTPRouteTemplateSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSpec. +func (in *ApplicationSpec) DeepCopy() *ApplicationSpec { + if in == nil { + return nil + } + out := new(ApplicationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationStatus) DeepCopyInto(out *ApplicationStatus) { + *out = *in + if in.CronJobStatuses != nil { + in, out := &in.CronJobStatuses, &out.CronJobStatuses + *out = make(map[string]batchv1.CronJobStatus, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.DeploymentStatus != nil { + in, out := &in.DeploymentStatus, &out.DeploymentStatus + *out = new(appsv1.DeploymentStatus) + (*in).DeepCopyInto(*out) + } + if in.IngressStatus != nil { + in, out := &in.IngressStatus, &out.IngressStatus + *out = new(networkingv1.IngressStatus) + (*in).DeepCopyInto(*out) + } + if in.JobStatuses != nil { + in, out := &in.JobStatuses, &out.JobStatuses + *out = make(map[string]batchv1.JobStatus, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.RolloutStatus != nil { + in, out := &in.RolloutStatus, &out.RolloutStatus + *out = new(argoproj_io_rolloutsv1alpha1.RolloutStatus) + (*in).DeepCopyInto(*out) + } + if in.StatefulSetStatus != nil { + in, out := &in.StatefulSetStatus, &out.StatefulSetStatus + *out = new(appsv1.StatefulSetStatus) + (*in).DeepCopyInto(*out) + } + if in.ServiceStatus != nil { + in, out := &in.ServiceStatus, &out.ServiceStatus + *out = new(v1.ServiceStatus) + (*in).DeepCopyInto(*out) + } + if in.HPAStatus != nil { + in, out := &in.HPAStatus, &out.HPAStatus + *out = new(autoscalingv2.HorizontalPodAutoscalerStatus) + (*in).DeepCopyInto(*out) + } + if in.HTTPRouteStatus != nil { + in, out := &in.HTTPRouteStatus, &out.HTTPRouteStatus + *out = new(HTTPRouteStatusSummary) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationStatus. +func (in *ApplicationStatus) DeepCopy() *ApplicationStatus { + if in == nil { + return nil + } + out := new(ApplicationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertManagerConfig) DeepCopyInto(out *CertManagerConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertManagerConfig. +func (in *CertManagerConfig) DeepCopy() *CertManagerConfig { + if in == nil { + return nil + } + out := new(CertManagerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseConfig) DeepCopyInto(out *ClickHouseConfig) { + *out = *in + in.Resources.DeepCopyInto(&out.Resources) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseConfig. +func (in *ClickHouseConfig) DeepCopy() *ClickHouseConfig { + if in == nil { + return nil + } + out := new(ClickHouseConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseConnection) DeepCopyInto(out *ClickHouseConnection) { + *out = *in + in.Host.DeepCopyInto(&out.Host) + in.TCPPort.DeepCopyInto(&out.TCPPort) + in.HTTPPort.DeepCopyInto(&out.HTTPPort) + in.Database.DeepCopyInto(&out.Database) + in.Username.DeepCopyInto(&out.Username) + in.Password.DeepCopyInto(&out.Password) + in.URL.DeepCopyInto(&out.URL) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseConnection. +func (in *ClickHouseConnection) DeepCopy() *ClickHouseConnection { + if in == nil { + return nil + } + out := new(ClickHouseConnection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseInfraStatus) DeepCopyInto(out *ClickHouseInfraStatus) { + *out = *in + in.WBInfraStatus.DeepCopyInto(&out.WBInfraStatus) + in.Connection.DeepCopyInto(&out.Connection) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseInfraStatus. +func (in *ClickHouseInfraStatus) DeepCopy() *ClickHouseInfraStatus { + if in == nil { + return nil + } + out := new(ClickHouseInfraStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseKeeperSpec) DeepCopyInto(out *ClickHouseKeeperSpec) { + *out = *in + in.Config.DeepCopyInto(&out.Config) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseKeeperSpec. +func (in *ClickHouseKeeperSpec) DeepCopy() *ClickHouseKeeperSpec { + if in == nil { + return nil + } + out := new(ClickHouseKeeperSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseObjectStorageSpec) DeepCopyInto(out *ClickHouseObjectStorageSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseObjectStorageSpec. +func (in *ClickHouseObjectStorageSpec) DeepCopy() *ClickHouseObjectStorageSpec { + if in == nil { + return nil + } + out := new(ClickHouseObjectStorageSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseSpec) DeepCopyInto(out *ClickHouseSpec) { + *out = *in + if in.ManagedClickHouse != nil { + in, out := &in.ManagedClickHouse, &out.ManagedClickHouse + *out = new(ManagedClickHouseSpec) + (*in).DeepCopyInto(*out) + } + if in.ExternalClickHouse != nil { + in, out := &in.ExternalClickHouse, &out.ExternalClickHouse + *out = new(ClickHouseConnection) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseSpec. +func (in *ClickHouseSpec) DeepCopy() *ClickHouseSpec { + if in == nil { + return nil + } + out := new(ClickHouseSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayAPIConfig) DeepCopyInto(out *GatewayAPIConfig) { + *out = *in + in.Gateway.DeepCopyInto(&out.Gateway) + if in.ListenerName != nil { + in, out := &in.ListenerName, &out.ListenerName + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayAPIConfig. +func (in *GatewayAPIConfig) DeepCopy() *GatewayAPIConfig { + if in == nil { + return nil + } + out := new(GatewayAPIConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayConfig) DeepCopyInto(out *GatewayConfig) { + *out = *in + if in.GatewayRef != nil { + in, out := &in.GatewayRef, &out.GatewayRef + *out = new(GatewayReference) + **out = **in + } + if in.GatewayClassName != nil { + in, out := &in.GatewayClassName, &out.GatewayClassName + *out = new(string) + **out = **in + } + if in.Listeners != nil { + in, out := &in.Listeners, &out.Listeners + *out = make([]GatewayListener, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.InfrastructureAnnotations != nil { + in, out := &in.InfrastructureAnnotations, &out.InfrastructureAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayConfig. +func (in *GatewayConfig) DeepCopy() *GatewayConfig { + if in == nil { + return nil + } + out := new(GatewayConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayListener) DeepCopyInto(out *GatewayListener) { + *out = *in + if in.Hostname != nil { + in, out := &in.Hostname, &out.Hostname + *out = new(string) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(ListenerTLSConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayListener. +func (in *GatewayListener) DeepCopy() *GatewayListener { + if in == nil { + return nil + } + out := new(GatewayListener) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayReference) DeepCopyInto(out *GatewayReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayReference. +func (in *GatewayReference) DeepCopy() *GatewayReference { + if in == nil { + return nil + } + out := new(GatewayReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayStatusSummary) DeepCopyInto(out *GatewayStatusSummary) { + *out = *in + if in.Addresses != nil { + in, out := &in.Addresses, &out.Addresses + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.GatewayRef != nil { + in, out := &in.GatewayRef, &out.GatewayRef + *out = new(GatewayReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayStatusSummary. +func (in *GatewayStatusSummary) DeepCopy() *GatewayStatusSummary { + if in == nil { + return nil + } + out := new(GatewayStatusSummary) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GlobalSpec) DeepCopyInto(out *GlobalSpec) { + *out = *in + if in.CustomCACerts != nil { + in, out := &in.CustomCACerts, &out.CustomCACerts + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GlobalSpec. +func (in *GlobalSpec) DeepCopy() *GlobalSpec { + if in == nil { + return nil + } + out := new(GlobalSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPRouteStatusSummary) DeepCopyInto(out *HTTPRouteStatusSummary) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRouteStatusSummary. +func (in *HTTPRouteStatusSummary) DeepCopy() *HTTPRouteStatusSummary { + if in == nil { + return nil + } + out := new(HTTPRouteStatusSummary) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPRouteTemplateSpec) DeepCopyInto(out *HTTPRouteTemplateSpec) { + *out = *in + if in.ParentRefs != nil { + in, out := &in.ParentRefs, &out.ParentRefs + *out = make([]apisv1.ParentReference, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Hostnames != nil { + in, out := &in.Hostnames, &out.Hostnames + *out = make([]apisv1.Hostname, len(*in)) + copy(*out, *in) + } + if in.Paths != nil { + in, out := &in.Paths, &out.Paths + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ServicePort != nil { + in, out := &in.ServicePort, &out.ServicePort + *out = new(apisv1.PortNumber) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRouteTemplateSpec. +func (in *HTTPRouteTemplateSpec) DeepCopy() *HTTPRouteTemplateSpec { + if in == nil { + return nil + } + out := new(HTTPRouteTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressConfig) DeepCopyInto(out *IngressConfig) { + *out = *in + if in.IngressClassName != nil { + in, out := &in.IngressClassName, &out.IngressClassName + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressConfig. +func (in *IngressConfig) DeepCopy() *IngressConfig { + if in == nil { + return nil + } + out := new(IngressConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressStatusSummary) DeepCopyInto(out *IngressStatusSummary) { + *out = *in + if in.LoadBalancerIngress != nil { + in, out := &in.LoadBalancerIngress, &out.LoadBalancerIngress + *out = make([]v1.LoadBalancerIngress, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressStatusSummary. +func (in *IngressStatusSummary) DeepCopy() *IngressStatusSummary { + if in == nil { + return nil + } + out := new(IngressStatusSummary) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InternalServiceAuth) DeepCopyInto(out *InternalServiceAuth) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalServiceAuth. +func (in *InternalServiceAuth) DeepCopy() *InternalServiceAuth { + if in == nil { + return nil + } + out := new(InternalServiceAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KafkaConfig) DeepCopyInto(out *KafkaConfig) { + *out = *in + in.Resources.DeepCopyInto(&out.Resources) + out.ReplicationConfig = in.ReplicationConfig +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KafkaConfig. +func (in *KafkaConfig) DeepCopy() *KafkaConfig { + if in == nil { + return nil + } + out := new(KafkaConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KafkaConnection) DeepCopyInto(out *KafkaConnection) { + *out = *in + in.Host.DeepCopyInto(&out.Host) + in.Port.DeepCopyInto(&out.Port) + in.BrokerEndpoint.DeepCopyInto(&out.BrokerEndpoint) + in.ClusterID.DeepCopyInto(&out.ClusterID) + in.URL.DeepCopyInto(&out.URL) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KafkaConnection. +func (in *KafkaConnection) DeepCopy() *KafkaConnection { + if in == nil { + return nil + } + out := new(KafkaConnection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KafkaInfraStatus) DeepCopyInto(out *KafkaInfraStatus) { + *out = *in + in.WBInfraStatus.DeepCopyInto(&out.WBInfraStatus) + in.Connection.DeepCopyInto(&out.Connection) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KafkaInfraStatus. +func (in *KafkaInfraStatus) DeepCopy() *KafkaInfraStatus { + if in == nil { + return nil + } + out := new(KafkaInfraStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KafkaReplicationConfig) DeepCopyInto(out *KafkaReplicationConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KafkaReplicationConfig. +func (in *KafkaReplicationConfig) DeepCopy() *KafkaReplicationConfig { + if in == nil { + return nil + } + out := new(KafkaReplicationConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KafkaSpec) DeepCopyInto(out *KafkaSpec) { + *out = *in + if in.ManagedKafka != nil { + in, out := &in.ManagedKafka, &out.ManagedKafka + *out = new(ManagedKafkaSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KafkaSpec. +func (in *KafkaSpec) DeepCopy() *KafkaSpec { + if in == nil { + return nil + } + out := new(KafkaSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LegacyOverrides) DeepCopyInto(out *LegacyOverrides) { + *out = *in + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LegacyOverrides. +func (in *LegacyOverrides) DeepCopy() *LegacyOverrides { + if in == nil { + return nil + } + out := new(LegacyOverrides) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ListenerTLSConfig) DeepCopyInto(out *ListenerTLSConfig) { + *out = *in + if in.Mode != nil { + in, out := &in.Mode, &out.Mode + *out = new(string) + **out = **in + } + if in.CertificateRef != nil { + in, out := &in.CertificateRef, &out.CertificateRef + *out = new(SecretRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ListenerTLSConfig. +func (in *ListenerTLSConfig) DeepCopy() *ListenerTLSConfig { + if in == nil { + return nil + } + out := new(ListenerTLSConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedClickHouseSpec) DeepCopyInto(out *ManagedClickHouseSpec) { + *out = *in + in.ManagedInfraSpec.DeepCopyInto(&out.ManagedInfraSpec) + in.Config.DeepCopyInto(&out.Config) + out.Telemetry = in.Telemetry + out.ObjectStorage = in.ObjectStorage + in.Keeper.DeepCopyInto(&out.Keeper) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedClickHouseSpec. +func (in *ManagedClickHouseSpec) DeepCopy() *ManagedClickHouseSpec { + if in == nil { + return nil + } + out := new(ManagedClickHouseSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedInfraSpec) DeepCopyInto(out *ManagedInfraSpec) { + *out = *in + if in.RetentionPolicy != nil { + in, out := &in.RetentionPolicy, &out.RetentionPolicy + *out = new(RetentionPolicy) + **out = **in + } + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(v1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = new([]v1.Toleration) + if **in != nil { + in, out := *in, *out + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedInfraSpec. +func (in *ManagedInfraSpec) DeepCopy() *ManagedInfraSpec { + if in == nil { + return nil + } + out := new(ManagedInfraSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedKafkaSpec) DeepCopyInto(out *ManagedKafkaSpec) { + *out = *in + in.ManagedInfraSpec.DeepCopyInto(&out.ManagedInfraSpec) + in.Config.DeepCopyInto(&out.Config) + out.Telemetry = in.Telemetry +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedKafkaSpec. +func (in *ManagedKafkaSpec) DeepCopy() *ManagedKafkaSpec { + if in == nil { + return nil + } + out := new(ManagedKafkaSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedMysqlSpec) DeepCopyInto(out *ManagedMysqlSpec) { + *out = *in + in.ManagedInfraSpec.DeepCopyInto(&out.ManagedInfraSpec) + in.Config.DeepCopyInto(&out.Config) + out.Telemetry = in.Telemetry +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedMysqlSpec. +func (in *ManagedMysqlSpec) DeepCopy() *ManagedMysqlSpec { + if in == nil { + return nil + } + out := new(ManagedMysqlSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedObjectStoreSpec) DeepCopyInto(out *ManagedObjectStoreSpec) { + *out = *in + in.ManagedInfraSpec.DeepCopyInto(&out.ManagedInfraSpec) + out.SeaweedObjectStoreSpec = in.SeaweedObjectStoreSpec + in.Config.DeepCopyInto(&out.Config) + out.Telemetry = in.Telemetry +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedObjectStoreSpec. +func (in *ManagedObjectStoreSpec) DeepCopy() *ManagedObjectStoreSpec { + if in == nil { + return nil + } + out := new(ManagedObjectStoreSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedRedisSpec) DeepCopyInto(out *ManagedRedisSpec) { + *out = *in + in.ManagedInfraSpec.DeepCopyInto(&out.ManagedInfraSpec) + in.Config.DeepCopyInto(&out.Config) + in.Sentinel.DeepCopyInto(&out.Sentinel) + out.Telemetry = in.Telemetry +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedRedisSpec. +func (in *ManagedRedisSpec) DeepCopy() *ManagedRedisSpec { + if in == nil { + return nil + } + out := new(ManagedRedisSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MigrationJobStatus) DeepCopyInto(out *MigrationJobStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MigrationJobStatus. +func (in *MigrationJobStatus) DeepCopy() *MigrationJobStatus { + if in == nil { + return nil + } + out := new(MigrationJobStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MySQLConfig) DeepCopyInto(out *MySQLConfig) { + *out = *in + in.Resources.DeepCopyInto(&out.Resources) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MySQLConfig. +func (in *MySQLConfig) DeepCopy() *MySQLConfig { + if in == nil { + return nil + } + out := new(MySQLConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MySQLSpec) DeepCopyInto(out *MySQLSpec) { + *out = *in + if in.ManagedMysql != nil { + in, out := &in.ManagedMysql, &out.ManagedMysql + *out = new(ManagedMysqlSpec) + (*in).DeepCopyInto(*out) + } + if in.ExternalMysql != nil { + in, out := &in.ExternalMysql, &out.ExternalMysql + *out = new(MysqlConnection) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MySQLSpec. +func (in *MySQLSpec) DeepCopy() *MySQLSpec { + if in == nil { + return nil + } + out := new(MySQLSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MysqlConnection) DeepCopyInto(out *MysqlConnection) { + *out = *in + in.Host.DeepCopyInto(&out.Host) + in.Port.DeepCopyInto(&out.Port) + in.Database.DeepCopyInto(&out.Database) + in.Username.DeepCopyInto(&out.Username) + in.Password.DeepCopyInto(&out.Password) + in.Tls.DeepCopyInto(&out.Tls) + in.SslCa.DeepCopyInto(&out.SslCa) + in.SslCert.DeepCopyInto(&out.SslCert) + in.SslKey.DeepCopyInto(&out.SslKey) + in.URL.DeepCopyInto(&out.URL) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MysqlConnection. +func (in *MysqlConnection) DeepCopy() *MysqlConnection { + if in == nil { + return nil + } + out := new(MysqlConnection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MysqlInfraStatus) DeepCopyInto(out *MysqlInfraStatus) { + *out = *in + in.WBInfraStatus.DeepCopyInto(&out.WBInfraStatus) + in.Connection.DeepCopyInto(&out.Connection) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MysqlInfraStatus. +func (in *MysqlInfraStatus) DeepCopy() *MysqlInfraStatus { + if in == nil { + return nil + } + out := new(MysqlInfraStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkingSpec) DeepCopyInto(out *NetworkingSpec) { + *out = *in + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = new(IngressConfig) + (*in).DeepCopyInto(*out) + } + if in.GatewayAPI != nil { + in, out := &in.GatewayAPI, &out.GatewayAPI + *out = new(GatewayAPIConfig) + (*in).DeepCopyInto(*out) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSConfig) + (*in).DeepCopyInto(*out) + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkingSpec. +func (in *NetworkingSpec) DeepCopy() *NetworkingSpec { + if in == nil { + return nil + } + out := new(NetworkingSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectStoreConfig) DeepCopyInto(out *ObjectStoreConfig) { + *out = *in + in.Resources.DeepCopyInto(&out.Resources) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectStoreConfig. +func (in *ObjectStoreConfig) DeepCopy() *ObjectStoreConfig { + if in == nil { + return nil + } + out := new(ObjectStoreConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectStoreConnection) DeepCopyInto(out *ObjectStoreConnection) { + *out = *in + in.Provider.DeepCopyInto(&out.Provider) + in.Endpoint.DeepCopyInto(&out.Endpoint) + in.Port.DeepCopyInto(&out.Port) + in.AccessKey.DeepCopyInto(&out.AccessKey) + in.SecretKey.DeepCopyInto(&out.SecretKey) + in.Bucket.DeepCopyInto(&out.Bucket) + in.Path.DeepCopyInto(&out.Path) + in.Region.DeepCopyInto(&out.Region) + in.TlsEnabled.DeepCopyInto(&out.TlsEnabled) + in.ForcePathStyle.DeepCopyInto(&out.ForcePathStyle) + in.URL.DeepCopyInto(&out.URL) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectStoreConnection. +func (in *ObjectStoreConnection) DeepCopy() *ObjectStoreConnection { + if in == nil { + return nil + } + out := new(ObjectStoreConnection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectStoreInfraStatus) DeepCopyInto(out *ObjectStoreInfraStatus) { + *out = *in + in.WBInfraStatus.DeepCopyInto(&out.WBInfraStatus) + in.Connection.DeepCopyInto(&out.Connection) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectStoreInfraStatus. +func (in *ObjectStoreInfraStatus) DeepCopy() *ObjectStoreInfraStatus { + if in == nil { + return nil + } + out := new(ObjectStoreInfraStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectStoreSpec) DeepCopyInto(out *ObjectStoreSpec) { + *out = *in + if in.ManagedObjectStore != nil { + in, out := &in.ManagedObjectStore, &out.ManagedObjectStore + *out = new(ManagedObjectStoreSpec) + (*in).DeepCopyInto(*out) + } + if in.ExternalObjectStore != nil { + in, out := &in.ExternalObjectStore, &out.ExternalObjectStore + *out = new(ObjectStoreConnection) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectStoreSpec. +func (in *ObjectStoreSpec) DeepCopy() *ObjectStoreSpec { + if in == nil { + return nil + } + out := new(ObjectStoreSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OidcSpec) DeepCopyInto(out *OidcSpec) { + *out = *in + in.ClientId.DeepCopyInto(&out.ClientId) + in.ClientSecret.DeepCopyInto(&out.ClientSecret) + in.IssuerUrl.DeepCopyInto(&out.IssuerUrl) + in.AuthMethod.DeepCopyInto(&out.AuthMethod) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OidcSpec. +func (in *OidcSpec) DeepCopy() *OidcSpec { + if in == nil { + return nil + } + out := new(OidcSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisConfig) DeepCopyInto(out *RedisConfig) { + *out = *in + in.Resources.DeepCopyInto(&out.Resources) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisConfig. +func (in *RedisConfig) DeepCopy() *RedisConfig { + if in == nil { + return nil + } + out := new(RedisConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisConnection) DeepCopyInto(out *RedisConnection) { + *out = *in + in.Host.DeepCopyInto(&out.Host) + in.Port.DeepCopyInto(&out.Port) + in.Password.DeepCopyInto(&out.Password) + in.Tls.DeepCopyInto(&out.Tls) + in.SslCa.DeepCopyInto(&out.SslCa) + in.URL.DeepCopyInto(&out.URL) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisConnection. +func (in *RedisConnection) DeepCopy() *RedisConnection { + if in == nil { + return nil + } + out := new(RedisConnection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisInfraStatus) DeepCopyInto(out *RedisInfraStatus) { + *out = *in + in.WBInfraStatus.DeepCopyInto(&out.WBInfraStatus) + in.Connection.DeepCopyInto(&out.Connection) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisInfraStatus. +func (in *RedisInfraStatus) DeepCopy() *RedisInfraStatus { + if in == nil { + return nil + } + out := new(RedisInfraStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSentinelConfig) DeepCopyInto(out *RedisSentinelConfig) { + *out = *in + in.Resources.DeepCopyInto(&out.Resources) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSentinelConfig. +func (in *RedisSentinelConfig) DeepCopy() *RedisSentinelConfig { + if in == nil { + return nil + } + out := new(RedisSentinelConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSentinelSpec) DeepCopyInto(out *RedisSentinelSpec) { + *out = *in + in.Config.DeepCopyInto(&out.Config) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSentinelSpec. +func (in *RedisSentinelSpec) DeepCopy() *RedisSentinelSpec { + if in == nil { + return nil + } + out := new(RedisSentinelSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSpec) DeepCopyInto(out *RedisSpec) { + *out = *in + if in.ManagedRedis != nil { + in, out := &in.ManagedRedis, &out.ManagedRedis + *out = new(ManagedRedisSpec) + (*in).DeepCopyInto(*out) + } + if in.ExternalRedis != nil { + in, out := &in.ExternalRedis, &out.ExternalRedis + *out = new(RedisConnection) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSpec. +func (in *RedisSpec) DeepCopy() *RedisSpec { + if in == nil { + return nil + } + out := new(RedisSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RetentionPolicy) DeepCopyInto(out *RetentionPolicy) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RetentionPolicy. +func (in *RetentionPolicy) DeepCopy() *RetentionPolicy { + if in == nil { + return nil + } + out := new(RetentionPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeaweedObjectStoreSpec) DeepCopyInto(out *SeaweedObjectStoreSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeaweedObjectStoreSpec. +func (in *SeaweedObjectStoreSpec) DeepCopy() *SeaweedObjectStoreSpec { + if in == nil { + return nil + } + out := new(SeaweedObjectStoreSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretRef) DeepCopyInto(out *SecretRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretRef. +func (in *SecretRef) DeepCopy() *SecretRef { + if in == nil { + return nil + } + out := new(SecretRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccountSpec) DeepCopyInto(out *ServiceAccountSpec) { + *out = *in + if in.Create != nil { + in, out := &in.Create, &out.Create + *out = new(bool) + **out = **in + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountSpec. +func (in *ServiceAccountSpec) DeepCopy() *ServiceAccountSpec { + if in == nil { + return nil + } + out := new(ServiceAccountSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSConfig) DeepCopyInto(out *TLSConfig) { + *out = *in + if in.CertManager != nil { + in, out := &in.CertManager, &out.CertManager + *out = new(CertManagerConfig) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSConfig. +func (in *TLSConfig) DeepCopy() *TLSConfig { + if in == nil { + return nil + } + out := new(TLSConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Telemetry) DeepCopyInto(out *Telemetry) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Telemetry. +func (in *Telemetry) DeepCopy() *Telemetry { + if in == nil { + return nil + } + out := new(Telemetry) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TelemetryConnectionStatus) DeepCopyInto(out *TelemetryConnectionStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TelemetryConnectionStatus. +func (in *TelemetryConnectionStatus) DeepCopy() *TelemetryConnectionStatus { + if in == nil { + return nil + } + out := new(TelemetryConnectionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TelemetryInfraStatus) DeepCopyInto(out *TelemetryInfraStatus) { + *out = *in + in.WBInfraStatus.DeepCopyInto(&out.WBInfraStatus) + out.Connection = in.Connection +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TelemetryInfraStatus. +func (in *TelemetryInfraStatus) DeepCopy() *TelemetryInfraStatus { + if in == nil { + return nil + } + out := new(TelemetryInfraStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WBInfraStatus) DeepCopyInto(out *WBInfraStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WBInfraStatus. +func (in *WBInfraStatus) DeepCopy() *WBInfraStatus { + if in == nil { + return nil + } + out := new(WBInfraStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WandbAppSpec) DeepCopyInto(out *WandbAppSpec) { + *out = *in + if in.Features != nil { + in, out := &in.Features, &out.Features + *out = make(map[string]bool, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.InternalServiceAuth.DeepCopyInto(&out.InternalServiceAuth) + in.ServiceAccount.DeepCopyInto(&out.ServiceAccount) + in.Probes.DeepCopyInto(&out.Probes) + if in.AdditionalHostnames != nil { + in, out := &in.AdditionalHostnames, &out.AdditionalHostnames + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.OIDC.DeepCopyInto(&out.OIDC) + if in.LegacyOverrides != nil { + in, out := &in.LegacyOverrides, &out.LegacyOverrides + *out = make(map[string]LegacyOverrides, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WandbAppSpec. +func (in *WandbAppSpec) DeepCopy() *WandbAppSpec { + if in == nil { + return nil + } + out := new(WandbAppSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WandbMigrationStatus) DeepCopyInto(out *WandbMigrationStatus) { + *out = *in + if in.Jobs != nil { + in, out := &in.Jobs, &out.Jobs + *out = make(map[string]MigrationJobStatus, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WandbMigrationStatus. +func (in *WandbMigrationStatus) DeepCopy() *WandbMigrationStatus { + if in == nil { + return nil + } + out := new(WandbMigrationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WandbProbeDefaults) DeepCopyInto(out *WandbProbeDefaults) { + *out = *in + if in.StartupProbe != nil { + in, out := &in.StartupProbe, &out.StartupProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.LivenessProbe != nil { + in, out := &in.LivenessProbe, &out.LivenessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.ReadinessProbe != nil { + in, out := &in.ReadinessProbe, &out.ReadinessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WandbProbeDefaults. +func (in *WandbProbeDefaults) DeepCopy() *WandbProbeDefaults { + if in == nil { + return nil + } + out := new(WandbProbeDefaults) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WandbStatus) DeepCopyInto(out *WandbStatus) { + *out = *in + if in.Applications != nil { + in, out := &in.Applications, &out.Applications + *out = make(map[string]ApplicationStatus, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + in.Migration.DeepCopyInto(&out.Migration) + if in.MySQLInit != nil { + in, out := &in.MySQLInit, &out.MySQLInit + *out = make(map[string]MigrationJobStatus, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WandbStatus. +func (in *WandbStatus) DeepCopy() *WandbStatus { + if in == nil { + return nil + } + out := new(WandbStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WeightsAndBiases) DeepCopyInto(out *WeightsAndBiases) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WeightsAndBiases. +func (in *WeightsAndBiases) DeepCopy() *WeightsAndBiases { + if in == nil { + return nil + } + out := new(WeightsAndBiases) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WeightsAndBiases) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WeightsAndBiasesList) DeepCopyInto(out *WeightsAndBiasesList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]WeightsAndBiases, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WeightsAndBiasesList. +func (in *WeightsAndBiasesList) DeepCopy() *WeightsAndBiasesList { + if in == nil { + return nil + } + out := new(WeightsAndBiasesList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WeightsAndBiasesList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WeightsAndBiasesSpec) DeepCopyInto(out *WeightsAndBiasesSpec) { + *out = *in + out.RetentionPolicy = in.RetentionPolicy + in.Global.DeepCopyInto(&out.Global) + in.Wandb.DeepCopyInto(&out.Wandb) + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(v1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = new([]v1.Toleration) + if **in != nil { + in, out := *in, *out + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.MySQL != nil { + in, out := &in.MySQL, &out.MySQL + *out = make(map[string]MySQLSpec, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.Redis != nil { + in, out := &in.Redis, &out.Redis + *out = make(map[string]RedisSpec, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + in.Kafka.DeepCopyInto(&out.Kafka) + if in.ObjectStore != nil { + in, out := &in.ObjectStore, &out.ObjectStore + *out = make(map[string]ObjectStoreSpec, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.ClickHouse != nil { + in, out := &in.ClickHouse, &out.ClickHouse + *out = make(map[string]ClickHouseSpec, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + in.Networking.DeepCopyInto(&out.Networking) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WeightsAndBiasesSpec. +func (in *WeightsAndBiasesSpec) DeepCopy() *WeightsAndBiasesSpec { + if in == nil { + return nil + } + out := new(WeightsAndBiasesSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WeightsAndBiasesStatus) DeepCopyInto(out *WeightsAndBiasesStatus) { + *out = *in + in.Wandb.DeepCopyInto(&out.Wandb) + if in.MySQLStatus != nil { + in, out := &in.MySQLStatus, &out.MySQLStatus + *out = make(map[string]MysqlInfraStatus, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.RedisStatus != nil { + in, out := &in.RedisStatus, &out.RedisStatus + *out = make(map[string]RedisInfraStatus, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + in.KafkaStatus.DeepCopyInto(&out.KafkaStatus) + if in.ObjectStoreStatus != nil { + in, out := &in.ObjectStoreStatus, &out.ObjectStoreStatus + *out = make(map[string]ObjectStoreInfraStatus, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.ClickHouseStatus != nil { + in, out := &in.ClickHouseStatus, &out.ClickHouseStatus + *out = make(map[string]ClickHouseInfraStatus, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + in.TelemetryStatus.DeepCopyInto(&out.TelemetryStatus) + if in.GeneratedSecrets != nil { + in, out := &in.GeneratedSecrets, &out.GeneratedSecrets + *out = make(map[string]v1.SecretKeySelector, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.GatewayStatus != nil { + in, out := &in.GatewayStatus, &out.GatewayStatus + *out = new(GatewayStatusSummary) + (*in).DeepCopyInto(*out) + } + if in.IngressStatus != nil { + in, out := &in.IngressStatus, &out.IngressStatus + *out = new(IngressStatusSummary) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WeightsAndBiasesStatus. +func (in *WeightsAndBiasesStatus) DeepCopy() *WeightsAndBiasesStatus { + if in == nil { + return nil + } + out := new(WeightsAndBiasesStatus) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/crd-installer/main.go b/cmd/crd-installer/main.go new file mode 100644 index 00000000..5f3954f5 --- /dev/null +++ b/cmd/crd-installer/main.go @@ -0,0 +1,207 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "flag" + "fmt" + "log/slog" + "os" + "os/signal" + "strings" + "syscall" + + apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "sigs.k8s.io/controller-runtime/pkg/client/config" + + "github.com/wandb/operator/internal/crdinstaller" + "github.com/wandb/operator/internal/logx" +) + +// crd-installer installs Custom Resource Definitions used by the wandb +// operator and its optional subcharts. It is invoked as a pre-install / +// pre-upgrade Helm hook from the operator chart. +// +// Subcommands: +// +// render compose the CRDs and emit YAML to stdout (debugging / dry-run) +// apply compose the CRDs and server-side apply them to the cluster +// +// Every flag also accepts the matching UPPER_SNAKE_CASE env var. Required +// flags differ per subcommand — see usage. +func main() { + if len(os.Args) < 2 { + usage() + os.Exit(2) + } + sub := os.Args[1] + args := os.Args[2:] + + switch sub { + case "render": + os.Exit(runRender(args)) + case "apply": + os.Exit(runApply(args)) + case "-h", "--help", "help": + usage() + os.Exit(0) + default: + fmt.Fprintf(os.Stderr, "unknown subcommand %q\n\n", sub) + usage() + os.Exit(2) + } +} + +func usage() { + fmt.Fprint(os.Stderr, `crd-installer — install operator CRDs (server-side apply) or render them to stdout. + +usage: + crd-installer render [flags] + crd-installer apply [flags] + +flags (each also accepts the matching UPPER_SNAKE_CASE env var, e.g. CERT_INJECT_REFERENCE): + --cert-inject-reference value for cert-manager.io/inject-ca-from on operator CRDs (required) + --webhook-service-name conversion webhook service name on operator CRDs (required) + --webhook-service-namespace conversion webhook service namespace on operator CRDs (required) + --groups comma-separated optional CRD groups to include (e.g. "redis,kafka") +`) +} + +type cliOpts struct { + opts crdinstaller.Options + groupsRaw string + logLevel string + logFormat string +} + +func registerFlags(fs *flag.FlagSet, c *cliOpts) { + fs.StringVar(&c.opts.CertInjectReference, "cert-inject-reference", "", "value for cert-manager.io/inject-ca-from on operator CRDs") + fs.StringVar(&c.opts.WebhookServiceName, "webhook-service-name", "", "conversion webhook service name on operator CRDs") + fs.StringVar(&c.opts.WebhookServiceNamespace, "webhook-service-namespace", "", "conversion webhook service namespace on operator CRDs") + fs.StringVar(&c.groupsRaw, "groups", "", `comma-separated optional CRD groups to include (e.g. "redis,kafka")`) + fs.StringVar(&c.logLevel, "log-level", "info", "log level: debug, info, warn, error") + fs.StringVar(&c.logFormat, "log-format", "text", "log format: text or json") +} + +// applyEnvDefaults populates any flag whose user-supplied value is empty +// from the matching UPPER_SNAKE_CASE env var. Mirrors the manager binary's +// setFlagsFromEnvironment but only fills empties, so explicit CLI flags win. +func applyEnvDefaults(fs *flag.FlagSet) error { + var firstErr error + fs.VisitAll(func(f *flag.Flag) { + if firstErr != nil { + return + } + // Only override if the flag is still at its zero/default value. + if f.Value.String() != f.DefValue { + return + } + envKey := strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_")) + if v, ok := os.LookupEnv(envKey); ok { + if err := fs.Set(f.Name, v); err != nil { + firstErr = fmt.Errorf("setting --%s from %s: %w", f.Name, envKey, err) + } + } + }) + return firstErr +} + +func parseSubcommand(name string, args []string) (*cliOpts, error) { + fs := flag.NewFlagSet(name, flag.ContinueOnError) + c := &cliOpts{} + registerFlags(fs, c) + if err := fs.Parse(args); err != nil { + return nil, err + } + if err := applyEnvDefaults(fs); err != nil { + return nil, err + } + groups, err := crdinstaller.ParseGroups(c.groupsRaw) + if err != nil { + return nil, err + } + c.opts.Groups = groups + return c, nil +} + +func setupLogger(c *cliOpts) *slog.Logger { + var level slog.Level + switch strings.ToLower(c.logLevel) { + case "debug": + level = slog.LevelDebug + case "warn": + level = slog.LevelWarn + case "error": + level = slog.LevelError + default: + level = slog.LevelInfo + } + logx.SetOptions(&logx.Options{ + HandlerOptions: &slog.HandlerOptions{Level: level}, + // Logs go to stderr so `render`'s stdout stays a clean YAML stream. + Output: os.Stderr, + Format: logx.LogFormat(c.logFormat), + }) + return logx.NewSlogLogger("crd-installer") +} + +func runRender(args []string) int { + c, err := parseSubcommand("render", args) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return 2 + } + ctx, stop := signalContext() + defer stop() + if err := crdinstaller.Render(ctx, c.opts, os.Stdout); err != nil { + fmt.Fprintf(os.Stderr, "render failed: %v\n", err) + return 1 + } + return 0 +} + +func runApply(args []string) int { + c, err := parseSubcommand("apply", args) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return 2 + } + logger := setupLogger(c) + ctx, stop := signalContext() + defer stop() + + cfg, err := config.GetConfig() + if err != nil { + logger.Error("loading kubeconfig", "err", err) + return 1 + } + client, err := apiextensionsclient.NewForConfig(cfg) + if err != nil { + logger.Error("constructing apiextensions client", "err", err) + return 1 + } + if err := crdinstaller.Apply(ctx, c.opts, client, logger); err != nil { + logger.Error("apply failed", "err", err) + return 1 + } + return 0 +} + +func signalContext() (context.Context, context.CancelFunc) { + return signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) +} diff --git a/cmd/main.go b/cmd/manager/main.go similarity index 59% rename from cmd/main.go rename to cmd/manager/main.go index d69ac631..7edc029a 100644 --- a/cmd/main.go +++ b/cmd/manager/main.go @@ -20,29 +20,51 @@ import ( "crypto/tls" "flag" "fmt" + "log/slog" "os" "path/filepath" "strings" + gkeGatewayApiNetworkingv1 "github.com/GoogleCloudPlatform/gke-gateway-api/apis/networking/v1" + mocov1beta2 "github.com/cybozu-go/moco/api/v1beta2" + nginxGatewayv1alpha1 "github.com/nginx/nginx-gateway-fabric/apis/v1alpha1" + "github.com/wandb/operator/internal/logx" + "github.com/wandb/operator/pkg/utils" + chkv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1" + chiv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + argov1alpha1 "github.com/wandb/operator/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1" + redisv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redis/v1beta2" + redisreplicationv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redisreplication/v1beta2" + redissentinelv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redissentinel/v1beta2" + seaweedv1 "github.com/wandb/operator/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1" + "github.com/wandb/operator/pkg/wandb/spec/channel/deployer" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/discovery" + "sigs.k8s.io/controller-runtime/pkg/client/config" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. - "github.com/wandb/operator/internal/controller" - "github.com/wandb/operator/pkg/wandb/spec/channel/deployer" _ "k8s.io/client-go/plugin/pkg/client/auth" + "github.com/wandb/operator/internal/controller" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/certwatcher" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" - "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - "sigs.k8s.io/controller-runtime/pkg/webhook" + ctrlwh "sigs.k8s.io/controller-runtime/pkg/webhook" appsv1 "github.com/wandb/operator/api/v1" + appsv2 "github.com/wandb/operator/api/v2" + webhookv2 "github.com/wandb/operator/internal/webhook/v2" // +kubebuilder:scaffold:imports ) @@ -51,10 +73,24 @@ var ( setupLog = ctrl.Log.WithName("setup") ) +const defaultTelemetryConfigMapName = "wandb-operator-telemetry-config" + func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(appsv1.AddToScheme(scheme)) + utilruntime.Must(appsv2.AddToScheme(scheme)) + utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) + utilruntime.Must(redisv1beta2.AddToScheme(scheme)) + utilruntime.Must(redisreplicationv1beta2.AddToScheme(scheme)) + utilruntime.Must(redissentinelv1beta2.AddToScheme(scheme)) + utilruntime.Must(seaweedv1.AddToScheme(scheme)) + utilruntime.Must(chiv1.AddToScheme(scheme)) + utilruntime.Must(chkv1.AddToScheme(scheme)) + utilruntime.Must(mocov1beta2.AddToScheme(scheme)) + utilruntime.Must(gatewayv1.Install(scheme)) + utilruntime.Must(nginxGatewayv1alpha1.AddToScheme(scheme)) + utilruntime.Must(gkeGatewayApiNetworkingv1.Install(scheme)) // +kubebuilder:scaffold:scheme } @@ -69,7 +105,8 @@ func main() { var enableHTTP2 bool var tlsOpts []func(*tls.Config) var deployerAPI, isolationNamespaces string - var debug, airgapped bool + var debug, airgapped, enableV2, enableWebhooks, enableRollouts, openshift bool + var telemetryConfigName, telemetryConfigNamespace string flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") @@ -95,14 +132,55 @@ func main() { flag.BoolVar(&debug, "debug", false, "Enable debug mode") - opts := zap.Options{ - Development: true, - } - opts.BindFlags(flag.CommandLine) + flag.BoolVar(&enableV2, "enable-v2", true, "Use V2 of WandB CRD") + flag.BoolVar(&enableWebhooks, "enable-webhooks", true, "Enable webhooks") + flag.BoolVar(&enableRollouts, "enable-rollouts", false, "Enable Argo Rollout Support") + flag.BoolVar(&openshift, "openshift", false, "Enable OpenShift-compatible rendering for managed infrastructure pods") + flag.StringVar(&telemetryConfigName, "telemetry-config-name", "", "Optional override ConfigMap name containing operator telemetry configuration") + flag.StringVar(&telemetryConfigNamespace, "telemetry-config-namespace", "", "Optional override namespace containing the operator telemetry configuration ConfigMap") + + var logLevel = flag.String("log-level", "info", "Log level: debug, info, warn, error") + var logFormat = flag.String("log-format", "text", "Log format: text or json") + var logSource = flag.Bool("log-source", false, "Include source file/line in logs") + setFlagsFromEnvironment() flag.Parse() - ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + utils.SetOpenShiftMode(openshift) + + var slogLevel slog.Level + switch strings.ToLower(*logLevel) { + case "debug": + slogLevel = slog.LevelDebug + case "info": + slogLevel = slog.LevelInfo + case "warn": + slogLevel = slog.LevelWarn + case "error": + slogLevel = slog.LevelError + default: + slogLevel = slog.LevelInfo + } + + handlerOpts := &slog.HandlerOptions{ + Level: slogLevel, + AddSource: *logSource, + } + + logx.SetOptions(&logx.Options{ + HandlerOptions: handlerOpts, + Overrides: nil, + Output: os.Stderr, + Format: logx.LogFormat(*logFormat), + }) + + ctrl.SetLogger(logx.NewLogrLogger()) + + if enableRollouts { + utilruntime.Must(argov1alpha1.AddToScheme(scheme)) + } + + telemetryConfigRef := resolveTelemetryConfigRef(telemetryConfigName, telemetryConfigNamespace) // if the enable-http2 flag is false (the default), http/2 should be disabled // due to its vulnerabilities. More specifically, disabling http/2 will @@ -144,7 +222,7 @@ func main() { }) } - webhookServer := webhook.NewServer(webhook.Options{ + webhookServer := ctrlwh.NewServer(ctrlwh.Options{ TLSOpts: webhookTLSOpts, }) @@ -202,10 +280,20 @@ func main() { } cacheOptions.DefaultNamespaces = namespacesCacheConfig } + err := RegisterServerResources() + if err != nil { + setupLog.Error(err, "failed to register server resources") + os.Exit(1) + } mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ - Scheme: scheme, - Cache: cacheOptions, + Scheme: scheme, + Cache: cacheOptions, + Client: ctrlclient.Options{ + Cache: &ctrlclient.CacheOptions{ + DisableFor: []ctrlclient.Object{&corev1.PersistentVolumeClaim{}}, + }, + }, Metrics: metricsServerOptions, WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, @@ -229,16 +317,46 @@ func main() { } if err = (&controller.WeightsAndBiasesReconciler{ - IsAirgapped: airgapped, - Recorder: mgr.GetEventRecorderFor("weightsandbiases"), + IsAirgapped: airgapped, + Recorder: mgr.GetEventRecorderFor("weightsandbiases"), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + DeployerClient: &deployer.DeployerClient{DeployerAPI: deployerAPI}, + Debug: debug, + EnableV2: enableV2, + TelemetryConfigRef: telemetryConfigRef, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "WeightsAndBiases") + os.Exit(1) + } + + if err = (&controller.ApplicationReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - DeployerClient: &deployer.DeployerClient{DeployerAPI: deployerAPI}, - Debug: debug, + EnableRollouts: enableRollouts, }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "WeightsAndBiases") + setupLog.Error(err, "unable to create controller", "controller", "Application") os.Exit(1) } + + if enableWebhooks && enableV2 { + if err := webhookv2.SetupApplicationWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Application") + os.Exit(1) + } + } + + if enableWebhooks && enableV2 { + if err := webhookv2.SetupWeightsAndBiasesWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "WeightsAndBiases") + os.Exit(1) + } + } + + // Reader for auxiliary lookups (e.g. legacy Secret refs) during + // v1 → v2 conversion. Uses GetAPIReader to skip the informer cache, + // which may not be warm when the webhook starts serving. + appsv1.SetConversionReader(mgr.GetAPIReader()) // +kubebuilder:scaffold:builder if metricsCertWatcher != nil { @@ -277,7 +395,7 @@ func setFlagsFromEnvironment() []error { var errors []error flag.VisitAll(func(f *flag.Flag) { - name := strings.ToUpper(strings.Replace(f.Name, "-", "_", -1)) + name := strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_")) if value, ok := os.LookupEnv(name); ok { err := flag.Set(f.Name, value) if err != nil { @@ -288,3 +406,67 @@ func setFlagsFromEnvironment() []error { return errors } + +func resolveTelemetryConfigRef(nameOverride, namespaceOverride string) types.NamespacedName { + ref := types.NamespacedName{ + Name: strings.TrimSpace(nameOverride), + } + if ref.Name == "" { + ref.Name = defaultTelemetryConfigMapName + } + + ref.Namespace = strings.TrimSpace(namespaceOverride) + if ref.Namespace != "" { + return ref + } + + namespace, err := detectRuntimeNamespace() + if err != nil { + setupLog.Info("using default namespace for telemetry config because runtime namespace could not be detected", "error", err) + ref.Namespace = "default" + return ref + } + + ref.Namespace = namespace + return ref +} + +func detectRuntimeNamespace() (string, error) { + data, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") + if err != nil { + return "", err + } + + namespace := strings.TrimSpace(string(data)) + if namespace == "" { + return "", fmt.Errorf("runtime namespace file is empty") + } + + return namespace, nil +} + +func RegisterServerResources() error { + cfg, err := config.GetConfig() + if err != nil { + return err + } + + discoveryClient, err := discovery.NewDiscoveryClientForConfig(cfg) + if err != nil { + return err + } + + // Get all resources in the cluster + _, resourceLists, err := discoveryClient.ServerGroupsAndResources() + if err != nil { + return err + } + + for _, resourceList := range resourceLists { + for _, resource := range resourceList.APIResources { + gvkString := fmt.Sprintf("%s.%s/%s", resource.Kind, resource.Group, resource.Version) + utils.AddServerResource(gvkString) + } + } + return nil +} diff --git a/config/certmanager/certificate-metrics.yaml b/config/certmanager/certificate-metrics.yaml new file mode 100644 index 00000000..1125de29 --- /dev/null +++ b/config/certmanager/certificate-metrics.yaml @@ -0,0 +1,20 @@ +# The following manifests contain a self-signed issuer CR and a metrics certificate CR. +# More document can be found at https://docs.cert-manager.io +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: metrics-certs # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + dnsNames: + # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize + # replacements in the config/default/kustomization.yaml file. + - SERVICE_NAME.SERVICE_NAMESPACE.svc + - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: metrics-server-cert diff --git a/config/certmanager/certificate-webhook.yaml b/config/certmanager/certificate-webhook.yaml new file mode 100644 index 00000000..a839fa0e --- /dev/null +++ b/config/certmanager/certificate-webhook.yaml @@ -0,0 +1,20 @@ +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml + namespace: system +spec: + # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize + # replacements in the config/default/kustomization.yaml file. + dnsNames: + - SERVICE_NAME.SERVICE_NAMESPACE.svc + - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: webhook-server-cert diff --git a/config/certmanager/issuer.yaml b/config/certmanager/issuer.yaml new file mode 100644 index 00000000..82ee162c --- /dev/null +++ b/config/certmanager/issuer.yaml @@ -0,0 +1,13 @@ +# The following manifest contains a self-signed issuer CR. +# More information can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: selfsigned-issuer + namespace: system +spec: + selfSigned: {} diff --git a/config/certmanager/kustomization.yaml b/config/certmanager/kustomization.yaml new file mode 100644 index 00000000..fcb7498e --- /dev/null +++ b/config/certmanager/kustomization.yaml @@ -0,0 +1,7 @@ +resources: +- issuer.yaml +- certificate-webhook.yaml +- certificate-metrics.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/certmanager/kustomizeconfig.yaml b/config/certmanager/kustomizeconfig.yaml new file mode 100644 index 00000000..cf6f89e8 --- /dev/null +++ b/config/certmanager/kustomizeconfig.yaml @@ -0,0 +1,8 @@ +# This configuration is for teaching kustomize how to update name ref substitution +nameReference: +- kind: Issuer + group: cert-manager.io + fieldSpecs: + - kind: Certificate + group: cert-manager.io + path: spec/issuerRef/name diff --git a/config/crd/bases/apps.wandb.com_applications.yaml b/config/crd/bases/apps.wandb.com_applications.yaml new file mode 100644 index 00000000..67be4bc9 --- /dev/null +++ b/config/crd/bases/apps.wandb.com_applications.yaml @@ -0,0 +1,13929 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: applications.apps.wandb.com +spec: + group: apps.wandb.com + names: + kind: Application + listKind: ApplicationList + plural: applications + singular: application + scope: Namespaced + versions: + - name: v2 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + cronJobs: + items: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + concurrencyPolicy: + type: string + failedJobsHistoryLimit: + format: int32 + type: integer + jobTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + backoffLimit: + format: int32 + type: integer + backoffLimitPerIndex: + format: int32 + type: integer + completionMode: + type: string + completions: + format: int32 + type: integer + managedBy: + type: string + manualSelector: + type: boolean + maxFailedIndexes: + format: int32 + type: integer + parallelism: + format: int32 + type: integer + podFailurePolicy: + properties: + rules: + items: + properties: + action: + type: string + onExitCodes: + properties: + containerName: + type: string + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + items: + properties: + status: + type: string + type: + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + successPolicy: + properties: + rules: + items: + properties: + succeededCount: + format: int32 + type: integer + succeededIndexes: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + suspend: + type: boolean + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + x-kubernetes-list-type: atomic + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + searches: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + x-kubernetes-list-map-keys: + - ip + x-kubernetes-list-type: map + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + hostnameOverride: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + initContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + x-kubernetes-list-type: atomic + resourceClaims: + items: + properties: + name: + type: string + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + default: ext4 + type: string + kind: + type: string + readOnly: + default: false + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + default: default + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + default: /etc/ceph/keyring + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + default: xfs + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + default: ThinProvisioned + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + workloadRef: + properties: + name: + type: string + podGroup: + type: string + podGroupReplicaKey: + type: string + required: + - name + - podGroup + type: object + required: + - containers + type: object + type: object + ttlSecondsAfterFinished: + format: int32 + type: integer + required: + - template + type: object + type: object + schedule: + type: string + startingDeadlineSeconds: + format: int64 + type: integer + successfulJobsHistoryLimit: + format: int32 + type: integer + suspend: + type: boolean + timeZone: + type: string + required: + - jobTemplate + - schedule + type: object + status: + properties: + active: + items: + properties: + apiVersion: + type: string + fieldPath: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + resourceVersion: + type: string + uid: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + lastScheduleTime: + format: date-time + type: string + lastSuccessfulTime: + format: date-time + type: string + type: object + type: object + type: array + hpaTemplate: + properties: + behavior: + properties: + scaleDown: + properties: + policies: + items: + properties: + periodSeconds: + format: int32 + type: integer + type: + type: string + value: + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + type: string + stabilizationWindowSeconds: + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + properties: + policies: + items: + properties: + periodSeconds: + format: int32 + type: integer + type: + type: string + value: + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + type: string + stabilizationWindowSeconds: + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + maxReplicas: + format: int32 + type: integer + metrics: + items: + properties: + containerResource: + properties: + container: + type: string + name: + type: string + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + properties: + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + properties: + describedObject: + properties: + apiVersion: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + properties: + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + properties: + name: + type: string + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + minReplicas: + format: int32 + type: integer + scaleTargetRef: + properties: + apiVersion: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + required: + - maxReplicas + - scaleTargetRef + type: object + httpRouteTemplate: + properties: + hostnames: + items: + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + parentRefs: + items: + properties: + group: + default: gateway.networking.k8s.io + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + maxLength: 253 + minLength: 1 + type: string + namespace: + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + type: array + pathType: + type: string + paths: + items: + type: string + type: array + servicePort: + format: int32 + type: integer + required: + - parentRefs + type: object + ingressTemplate: + properties: + defaultBackend: + properties: + resource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + service: + properties: + name: + type: string + port: + properties: + name: + type: string + number: + format: int32 + type: integer + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + type: object + ingressClassName: + type: string + rules: + items: + properties: + host: + type: string + http: + properties: + paths: + items: + properties: + backend: + properties: + resource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + service: + properties: + name: + type: string + port: + properties: + name: + type: string + number: + format: int32 + type: integer + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + type: object + path: + type: string + pathType: + type: string + required: + - backend + - pathType + type: object + type: array + x-kubernetes-list-type: atomic + required: + - paths + type: object + type: object + type: array + x-kubernetes-list-type: atomic + tls: + items: + properties: + hosts: + items: + type: string + type: array + x-kubernetes-list-type: atomic + secretName: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + type: object + jobs: + items: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + backoffLimit: + format: int32 + type: integer + backoffLimitPerIndex: + format: int32 + type: integer + completionMode: + type: string + completions: + format: int32 + type: integer + managedBy: + type: string + manualSelector: + type: boolean + maxFailedIndexes: + format: int32 + type: integer + parallelism: + format: int32 + type: integer + podFailurePolicy: + properties: + rules: + items: + properties: + action: + type: string + onExitCodes: + properties: + containerName: + type: string + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + items: + properties: + status: + type: string + type: + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + successPolicy: + properties: + rules: + items: + properties: + succeededCount: + format: int32 + type: integer + succeededIndexes: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + suspend: + type: boolean + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + x-kubernetes-list-type: atomic + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + searches: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + x-kubernetes-list-map-keys: + - ip + x-kubernetes-list-type: map + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + hostnameOverride: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + initContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + x-kubernetes-list-type: atomic + resourceClaims: + items: + properties: + name: + type: string + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + default: ext4 + type: string + kind: + type: string + readOnly: + default: false + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + default: default + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + default: /etc/ceph/keyring + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + default: xfs + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + default: ThinProvisioned + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + workloadRef: + properties: + name: + type: string + podGroup: + type: string + podGroupReplicaKey: + type: string + required: + - name + - podGroup + type: object + required: + - containers + type: object + type: object + ttlSecondsAfterFinished: + format: int32 + type: integer + required: + - template + type: object + status: + properties: + active: + format: int32 + type: integer + completedIndexes: + type: string + completionTime: + format: date-time + type: string + conditions: + items: + properties: + lastProbeTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + failed: + format: int32 + type: integer + failedIndexes: + type: string + ready: + format: int32 + type: integer + startTime: + format: date-time + type: string + succeeded: + format: int32 + type: integer + terminating: + format: int32 + type: integer + uncountedTerminatedPods: + properties: + failed: + items: + type: string + type: array + x-kubernetes-list-type: set + succeeded: + items: + type: string + type: array + x-kubernetes-list-type: set + type: object + type: object + type: object + type: array + kind: + type: string + metaTemplate: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + pdbTemplate: + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + unhealthyPodEvictionPolicy: + type: string + type: object + podTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + x-kubernetes-list-type: atomic + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + searches: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + x-kubernetes-list-map-keys: + - ip + x-kubernetes-list-type: map + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + hostnameOverride: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + initContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + x-kubernetes-list-type: atomic + resourceClaims: + items: + properties: + name: + type: string + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + default: ext4 + type: string + kind: + type: string + readOnly: + default: false + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + default: default + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + default: /etc/ceph/keyring + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + default: xfs + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + default: ThinProvisioned + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + workloadRef: + properties: + name: + type: string + podGroup: + type: string + podGroupReplicaKey: + type: string + required: + - name + - podGroup + type: object + required: + - containers + type: object + type: object + replicas: + format: int32 + type: integer + scaledObjectTemplate: + properties: + advanced: + properties: + horizontalPodAutoscalerConfig: + properties: + behavior: + properties: + scaleDown: + properties: + policies: + items: + properties: + periodSeconds: + format: int32 + type: integer + type: + type: string + value: + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + type: string + stabilizationWindowSeconds: + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + properties: + policies: + items: + properties: + periodSeconds: + format: int32 + type: integer + type: + type: string + value: + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + type: string + stabilizationWindowSeconds: + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + name: + type: string + type: object + restoreToOriginalReplicaCount: + type: boolean + scalingModifiers: + properties: + activationTarget: + type: string + formula: + type: string + metricType: + enum: + - AverageValue + - Value + type: string + target: + type: string + type: object + type: object + cooldownPeriod: + format: int32 + type: integer + fallback: + properties: + behavior: + default: static + enum: + - static + - currentReplicas + - currentReplicasIfHigher + - currentReplicasIfLower + type: string + failureThreshold: + format: int32 + type: integer + replicas: + format: int32 + type: integer + required: + - failureThreshold + - replicas + type: object + idleReplicaCount: + format: int32 + type: integer + initialCooldownPeriod: + format: int32 + type: integer + maxReplicaCount: + format: int32 + type: integer + minReplicaCount: + format: int32 + type: integer + pollingInterval: + format: int32 + type: integer + scaleTargetRef: + properties: + apiVersion: + type: string + envSourceContainerName: + type: string + kind: + type: string + name: + type: string + required: + - name + type: object + triggers: + items: + properties: + authenticationRef: + properties: + kind: + type: string + name: + type: string + required: + - name + type: object + metadata: + additionalProperties: + type: string + type: object + metricType: + type: string + name: + type: string + type: + type: string + useCachedMetrics: + type: boolean + required: + - metadata + - type + type: object + type: array + required: + - scaleTargetRef + - triggers + type: object + serviceName: + type: string + serviceTemplate: + properties: + allocateLoadBalancerNodePorts: + type: boolean + clusterIP: + type: string + clusterIPs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + externalIPs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + externalName: + type: string + externalTrafficPolicy: + type: string + healthCheckNodePort: + format: int32 + type: integer + internalTrafficPolicy: + type: string + ipFamilies: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ipFamilyPolicy: + type: string + loadBalancerClass: + type: string + loadBalancerIP: + type: string + loadBalancerSourceRanges: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ports: + items: + properties: + appProtocol: + type: string + name: + type: string + nodePort: + format: int32 + type: integer + port: + format: int32 + type: integer + protocol: + default: TCP + type: string + targetPort: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + x-kubernetes-list-map-keys: + - port + - protocol + x-kubernetes-list-type: map + publishNotReadyAddresses: + type: boolean + selector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + sessionAffinity: + type: string + sessionAffinityConfig: + properties: + clientIP: + properties: + timeoutSeconds: + format: int32 + type: integer + type: object + type: object + trafficDistribution: + type: string + type: + type: string + type: object + volumeClaimTemplates: + items: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + status: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + allocatedResourceStatuses: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: granular + allocatedResources: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + capacity: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + conditions: + items: + properties: + lastProbeTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentVolumeAttributesClassName: + type: string + modifyVolumeStatus: + properties: + status: + type: string + targetVolumeAttributesClassName: + type: string + required: + - status + type: object + phase: + type: string + type: object + type: object + type: array + type: object + status: + properties: + cronJobStatuses: + additionalProperties: + properties: + active: + items: + properties: + apiVersion: + type: string + fieldPath: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + resourceVersion: + type: string + uid: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + lastScheduleTime: + format: date-time + type: string + lastSuccessfulTime: + format: date-time + type: string + type: object + type: object + deploymentStatus: + properties: + availableReplicas: + format: int32 + type: integer + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + format: int64 + type: integer + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + terminatingReplicas: + format: int32 + type: integer + unavailableReplicas: + format: int32 + type: integer + updatedReplicas: + format: int32 + type: integer + type: object + hpaStatus: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentMetrics: + items: + properties: + containerResource: + properties: + container: + type: string + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + name: + type: string + required: + - container + - current + - name + type: object + external: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - metric + type: object + object: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + describedObject: + properties: + apiVersion: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - describedObject + - metric + type: object + pods: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - metric + type: object + resource: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + name: + type: string + required: + - current + - name + type: object + type: + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + currentReplicas: + format: int32 + type: integer + desiredReplicas: + format: int32 + type: integer + lastScaleTime: + format: date-time + type: string + observedGeneration: + format: int64 + type: integer + required: + - desiredReplicas + type: object + httpRouteStatus: + properties: + accepted: + type: boolean + type: object + ingressStatus: + properties: + loadBalancer: + properties: + ingress: + items: + properties: + hostname: + type: string + ip: + type: string + ports: + items: + properties: + error: + maxLength: 316 + 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])$ + type: string + port: + format: int32 + type: integer + protocol: + type: string + required: + - error + - port + - protocol + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + jobStatuses: + additionalProperties: + properties: + active: + format: int32 + type: integer + completedIndexes: + type: string + completionTime: + format: date-time + type: string + conditions: + items: + properties: + lastProbeTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + failed: + format: int32 + type: integer + failedIndexes: + type: string + ready: + format: int32 + type: integer + startTime: + format: date-time + type: string + succeeded: + format: int32 + type: integer + terminating: + format: int32 + type: integer + uncountedTerminatedPods: + properties: + failed: + items: + type: string + type: array + x-kubernetes-list-type: set + succeeded: + items: + type: string + type: array + x-kubernetes-list-type: set + type: object + type: object + type: object + ready: + type: boolean + rolloutStatus: + properties: + HPAReplicas: + format: int32 + type: integer + abort: + type: boolean + abortedAt: + format: date-time + type: string + alb: + properties: + canaryTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + ingress: + type: string + loadBalancer: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + stableTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + type: object + albs: + items: + properties: + canaryTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + ingress: + type: string + loadBalancer: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + stableTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + type: object + type: array + availableReplicas: + format: int32 + type: integer + blueGreen: + properties: + activeSelector: + type: string + postPromotionAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + prePromotionAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + previewSelector: + type: string + scaleUpPreviewCheckPoint: + type: boolean + type: object + canary: + properties: + currentBackgroundAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + currentExperiment: + type: string + currentStepAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + stablePingPong: + type: string + stepPluginStatuses: + items: + properties: + backoff: + type: string + disabled: + type: boolean + executions: + format: int32 + type: integer + finishedAt: + format: date-time + type: string + index: + format: int32 + type: integer + message: + type: string + name: + type: string + operation: + type: string + phase: + type: string + startedAt: + format: date-time + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + updatedAt: + format: date-time + type: string + required: + - index + - name + - operation + type: object + type: array + weights: + properties: + additional: + items: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + type: array + canary: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + stable: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + verified: + type: boolean + required: + - canary + - stable + type: object + type: object + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - lastTransitionTime + - lastUpdateTime + - message + - reason + - status + - type + type: object + type: array + controllerPause: + type: boolean + currentPodHash: + type: string + currentStepHash: + type: string + currentStepIndex: + format: int32 + type: integer + message: + type: string + observedGeneration: + type: string + pauseConditions: + items: + properties: + reason: + type: string + startTime: + format: date-time + type: string + required: + - reason + - startTime + type: object + type: array + phase: + type: string + promoteFull: + type: boolean + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + restartedAt: + format: date-time + type: string + selector: + type: string + stableRS: + type: string + updatedReplicas: + format: int32 + type: integer + workloadObservedGeneration: + type: string + type: object + serviceStatus: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + loadBalancer: + properties: + ingress: + items: + properties: + hostname: + type: string + ip: + type: string + ipMode: + type: string + ports: + items: + properties: + error: + maxLength: 316 + 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])$ + type: string + port: + format: int32 + type: integer + protocol: + type: string + required: + - error + - port + - protocol + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + statefulSetStatus: + properties: + availableReplicas: + format: int32 + type: integer + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentReplicas: + format: int32 + type: integer + currentRevision: + type: string + observedGeneration: + format: int64 + type: integer + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + updateRevision: + type: string + updatedReplicas: + format: int32 + type: integer + required: + - replicas + type: object + required: + - ready + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/apps.wandb.com_weightsandbiases.yaml b/config/crd/bases/apps.wandb.com_weightsandbiases.yaml index d30fe3ce..5b5fa8d2 100644 --- a/config/crd/bases/apps.wandb.com_weightsandbiases.yaml +++ b/config/crd/bases/apps.wandb.com_weightsandbiases.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.17.1 + controller-gen.kubebuilder.io/version: v0.19.0 name: weightsandbiases.apps.wandb.com spec: group: apps.wandb.com @@ -19,39 +19,23 @@ spec: - name: v1 schema: openAPIV3Schema: - description: WeightsAndBiases is the Schema for the weightsandbiases API properties: apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object spec: - description: WeightsAndBiasesSpec defines the desired state of WeightsAndBiases properties: chart: - description: Unstructured values for rendering CDK8s Config. type: object x-kubernetes-preserve-unknown-fields: true values: - description: Unstructured values for rendering CDK8s Config. type: object x-kubernetes-preserve-unknown-fields: true type: object status: - description: WeightsAndBiasesStatus defines the observed state of WeightsAndBiases properties: phase: type: string @@ -60,6 +44,6201 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .status.ready + name: Ready + type: boolean + - jsonPath: .status.mysqlStatus.default.state + name: MySQL + type: string + - jsonPath: .status.redisStatus.default.state + name: Redis + type: string + - jsonPath: .status.kafkaStatus.state + name: Kafka + type: string + - jsonPath: .status.objectStoreStatus.default.state + name: ObjectStore + type: string + - jsonPath: .status.clickhouseStatus.default.state + name: ClickHouse + type: string + name: v2 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + clickhouse: + additionalProperties: + properties: + externalClickhouse: + properties: + database: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + httpPort: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tcpPort: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + username: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + managedClickhouse: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + config: + properties: + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + keeper: + properties: + config: + properties: + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + replicas: + format: int32 + type: integer + storageSize: + type: string + type: object + name: + type: string + namespace: + type: string + objectStorage: + properties: + insecure: + type: boolean + prefix: + type: string + type: object + replicas: + format: int32 + type: integer + retentionPolicy: + properties: + onDelete: + default: detach + type: string + required: + - onDelete + type: object + storageSize: + type: string + telemetry: + properties: + enabled: + default: true + type: boolean + required: + - enabled + type: object + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + version: + type: string + type: object + type: object + type: object + global: + properties: + caCertsConfigMap: + type: string + customCACerts: + items: + type: string + type: array + imageRegistry: + type: string + type: object + kafka: + properties: + managedKafka: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + config: + properties: + replicationConfig: + properties: + defaultReplicationFactor: + format: int32 + type: integer + minInSyncReplicas: + format: int32 + type: integer + offsetsTopicRF: + format: int32 + type: integer + transactionStateISR: + format: int32 + type: integer + transactionStateRF: + format: int32 + type: integer + type: object + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + name: + type: string + namespace: + type: string + replicas: + format: int32 + type: integer + retentionPolicy: + properties: + onDelete: + default: detach + type: string + required: + - onDelete + type: object + skipDataRecovery: + type: boolean + storageSize: + type: string + telemetry: + properties: + enabled: + default: true + type: boolean + required: + - enabled + type: object + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + type: object + type: object + mysql: + additionalProperties: + properties: + externalMysql: + properties: + database: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslCa: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslCert: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslKey: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tls: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + username: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + managedMysql: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + config: + properties: + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + name: + type: string + namespace: + type: string + replicas: + format: int32 + type: integer + retentionPolicy: + properties: + onDelete: + default: detach + type: string + required: + - onDelete + type: object + storageSize: + type: string + telemetry: + properties: + enabled: + default: true + type: boolean + required: + - enabled + type: object + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + type: object + type: object + type: object + networking: + properties: + annotations: + additionalProperties: + type: string + type: object + gatewayAPI: + properties: + gateway: + properties: + gatewayClassName: + type: string + gatewayRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + infrastructureAnnotations: + additionalProperties: + type: string + type: object + listeners: + items: + properties: + hostname: + type: string + name: + type: string + port: + format: int32 + type: integer + protocol: + type: string + tls: + properties: + certificateRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + mode: + type: string + type: object + required: + - name + - port + - protocol + type: object + type: array + managed: + default: false + type: boolean + type: object + listenerName: + type: string + required: + - gateway + type: object + ingress: + properties: + ingressClassName: + type: string + name: + type: string + type: object + mode: + enum: + - "" + - ingress + - gateway + type: string + tls: + properties: + certManager: + properties: + clusterIssuer: + type: string + issuer: + type: string + type: object + secretName: + type: string + type: object + type: object + objectStore: + additionalProperties: + properties: + externalObjectStore: + properties: + accessKey: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + bucket: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + endpoint: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + forcePathStyle: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + path: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + provider: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + region: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secretKey: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tlsEnabled: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + managedObjectStore: + properties: + SeaweedObjectStoreSpec: + properties: + filerStorageSize: + type: string + tlsEnabled: + type: boolean + type: object + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + config: + properties: + accessKey: + type: string + minioBrowserSetting: + type: string + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + rootUser: + type: string + type: object + copies: + format: int32 + type: integer + name: + type: string + namespace: + type: string + replicas: + format: int32 + type: integer + retentionPolicy: + properties: + onDelete: + default: detach + type: string + required: + - onDelete + type: object + storageSize: + type: string + telemetry: + properties: + enabled: + default: true + type: boolean + required: + - enabled + type: object + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + type: object + type: object + type: object + redis: + additionalProperties: + properties: + externalRedis: + properties: + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslCa: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tls: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + managedRedis: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + config: + properties: + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + name: + type: string + namespace: + type: string + retentionPolicy: + properties: + onDelete: + default: detach + type: string + required: + - onDelete + type: object + sentinel: + properties: + config: + properties: + masterName: + type: string + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + enabled: + type: boolean + required: + - enabled + type: object + storageSize: + type: string + telemetry: + properties: + enabled: + default: true + type: boolean + required: + - enabled + type: object + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + type: object + type: object + type: object + requireLimits: + type: boolean + retentionPolicy: + properties: + onDelete: + default: detach + type: string + required: + - onDelete + type: object + size: + enum: + - dev + - micro + - small + - medium + - large + - xlarge + - xxlarge + type: string + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + wandb: + properties: + additionalHostnames: + items: + type: string + type: array + bucketProxy: + type: boolean + features: + additionalProperties: + type: boolean + type: object + hostname: + type: string + internalServiceAuth: + properties: + enabled: + type: boolean + oidcIssuer: + type: string + type: object + legacyOverrides: + additionalProperties: + properties: + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + type: object + license: + type: string + manifestRepository: + type: string + oidc: + properties: + authMethod: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + clientId: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + clientSecret: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + issuerUrl: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sessionLength: + type: string + type: object + probes: + properties: + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + type: object + serviceAccount: + properties: + annotations: + additionalProperties: + type: string + type: object + create: + default: true + type: boolean + serviceAccountName: + default: wandb + type: string + required: + - create + type: object + version: + type: string + required: + - bucketProxy + - features + - hostname + - version + type: object + required: + - retentionPolicy + type: object + status: + properties: + clickhouseStatus: + additionalProperties: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + connection: + properties: + database: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + httpPort: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tcpPort: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + username: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + ready: + type: boolean + state: + type: string + required: + - ready + type: object + type: object + gatewayStatus: + properties: + addresses: + items: + type: string + type: array + gatewayRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + name: + type: string + ready: + type: boolean + type: object + generatedSecrets: + additionalProperties: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + ingressStatus: + properties: + loadBalancerIngress: + items: + properties: + hostname: + type: string + ip: + type: string + ipMode: + type: string + ports: + items: + properties: + error: + maxLength: 316 + 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])$ + type: string + port: + format: int32 + type: integer + protocol: + type: string + required: + - error + - port + - protocol + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + name: + type: string + type: object + kafkaStatus: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + connection: + properties: + brokerEndpoint: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + clusterID: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + ready: + type: boolean + state: + type: string + required: + - ready + type: object + mysqlStatus: + additionalProperties: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + connection: + properties: + database: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslCa: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslCert: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslKey: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tls: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + username: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + ready: + type: boolean + state: + type: string + required: + - ready + type: object + type: object + objectStoreStatus: + additionalProperties: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + connection: + properties: + accessKey: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + bucket: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + endpoint: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + forcePathStyle: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + path: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + provider: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + region: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secretKey: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tlsEnabled: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + ready: + type: boolean + state: + type: string + required: + - ready + type: object + type: object + observedGeneration: + format: int64 + type: integer + ready: + type: boolean + redisStatus: + additionalProperties: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + connection: + properties: + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslCa: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tls: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + ready: + type: boolean + state: + type: string + required: + - ready + type: object + type: object + telemetryStatus: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + connection: + properties: + connectionSecret: + type: string + datadogTraceAgentHost: + type: string + datadogTraceAgentPort: + type: string + datadogTraceAgentURL: + type: string + gorillaTracer: + type: string + logsEndpoint: + type: string + logsExporter: + type: string + managedNamespace: + type: string + metricsEndpoint: + type: string + metricsExporter: + type: string + protocol: + type: string + resourceAttributes: + type: string + serviceName: + type: string + statsdAddress: + type: string + tracesEndpoint: + type: string + tracesExporter: + type: string + type: object + mode: + type: string + ready: + type: boolean + state: + type: string + required: + - ready + type: object + wandb: + properties: + applications: + additionalProperties: + properties: + cronJobStatuses: + additionalProperties: + properties: + active: + items: + properties: + apiVersion: + type: string + fieldPath: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + resourceVersion: + type: string + uid: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + lastScheduleTime: + format: date-time + type: string + lastSuccessfulTime: + format: date-time + type: string + type: object + type: object + deploymentStatus: + properties: + availableReplicas: + format: int32 + type: integer + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + format: int64 + type: integer + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + terminatingReplicas: + format: int32 + type: integer + unavailableReplicas: + format: int32 + type: integer + updatedReplicas: + format: int32 + type: integer + type: object + hpaStatus: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentMetrics: + items: + properties: + containerResource: + properties: + container: + type: string + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + name: + type: string + required: + - container + - current + - name + type: object + external: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - metric + type: object + object: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + describedObject: + properties: + apiVersion: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - describedObject + - metric + type: object + pods: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - metric + type: object + resource: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + name: + type: string + required: + - current + - name + type: object + type: + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + currentReplicas: + format: int32 + type: integer + desiredReplicas: + format: int32 + type: integer + lastScaleTime: + format: date-time + type: string + observedGeneration: + format: int64 + type: integer + required: + - desiredReplicas + type: object + httpRouteStatus: + properties: + accepted: + type: boolean + type: object + ingressStatus: + properties: + loadBalancer: + properties: + ingress: + items: + properties: + hostname: + type: string + ip: + type: string + ports: + items: + properties: + error: + maxLength: 316 + 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])$ + type: string + port: + format: int32 + type: integer + protocol: + type: string + required: + - error + - port + - protocol + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + jobStatuses: + additionalProperties: + properties: + active: + format: int32 + type: integer + completedIndexes: + type: string + completionTime: + format: date-time + type: string + conditions: + items: + properties: + lastProbeTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + failed: + format: int32 + type: integer + failedIndexes: + type: string + ready: + format: int32 + type: integer + startTime: + format: date-time + type: string + succeeded: + format: int32 + type: integer + terminating: + format: int32 + type: integer + uncountedTerminatedPods: + properties: + failed: + items: + type: string + type: array + x-kubernetes-list-type: set + succeeded: + items: + type: string + type: array + x-kubernetes-list-type: set + type: object + type: object + type: object + ready: + type: boolean + rolloutStatus: + properties: + HPAReplicas: + format: int32 + type: integer + abort: + type: boolean + abortedAt: + format: date-time + type: string + alb: + properties: + canaryTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + ingress: + type: string + loadBalancer: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + stableTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + type: object + albs: + items: + properties: + canaryTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + ingress: + type: string + loadBalancer: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + stableTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + type: object + type: array + availableReplicas: + format: int32 + type: integer + blueGreen: + properties: + activeSelector: + type: string + postPromotionAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + prePromotionAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + previewSelector: + type: string + scaleUpPreviewCheckPoint: + type: boolean + type: object + canary: + properties: + currentBackgroundAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + currentExperiment: + type: string + currentStepAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + stablePingPong: + type: string + stepPluginStatuses: + items: + properties: + backoff: + type: string + disabled: + type: boolean + executions: + format: int32 + type: integer + finishedAt: + format: date-time + type: string + index: + format: int32 + type: integer + message: + type: string + name: + type: string + operation: + type: string + phase: + type: string + startedAt: + format: date-time + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + updatedAt: + format: date-time + type: string + required: + - index + - name + - operation + type: object + type: array + weights: + properties: + additional: + items: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + type: array + canary: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + stable: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + verified: + type: boolean + required: + - canary + - stable + type: object + type: object + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - lastTransitionTime + - lastUpdateTime + - message + - reason + - status + - type + type: object + type: array + controllerPause: + type: boolean + currentPodHash: + type: string + currentStepHash: + type: string + currentStepIndex: + format: int32 + type: integer + message: + type: string + observedGeneration: + type: string + pauseConditions: + items: + properties: + reason: + type: string + startTime: + format: date-time + type: string + required: + - reason + - startTime + type: object + type: array + phase: + type: string + promoteFull: + type: boolean + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + restartedAt: + format: date-time + type: string + selector: + type: string + stableRS: + type: string + updatedReplicas: + format: int32 + type: integer + workloadObservedGeneration: + type: string + type: object + serviceStatus: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + loadBalancer: + properties: + ingress: + items: + properties: + hostname: + type: string + ip: + type: string + ipMode: + type: string + ports: + items: + properties: + error: + maxLength: 316 + 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])$ + type: string + port: + format: int32 + type: integer + protocol: + type: string + required: + - error + - port + - protocol + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + statefulSetStatus: + properties: + availableReplicas: + format: int32 + type: integer + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentReplicas: + format: int32 + type: integer + currentRevision: + type: string + observedGeneration: + format: int64 + type: integer + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + updateRevision: + type: string + updatedReplicas: + format: int32 + type: integer + required: + - replicas + type: object + required: + - ready + type: object + default: {} + type: object + hostname: + type: string + migration: + properties: + jobs: + additionalProperties: + properties: + failed: + type: boolean + message: + type: string + name: + type: string + succeeded: + type: boolean + type: object + type: object + lastSuccessVersion: + type: string + ready: + type: boolean + reason: + type: string + version: + type: string + type: object + mysqlInit: + additionalProperties: + properties: + failed: + type: boolean + message: + type: string + name: + type: string + succeeded: + type: boolean + type: object + default: {} + type: object + required: + - hostname + type: object + required: + - observedGeneration + - ready + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/config/crd/bases/kustomization.yaml b/config/crd/bases/kustomization.yaml new file mode 100644 index 00000000..1ab16d8b --- /dev/null +++ b/config/crd/bases/kustomization.yaml @@ -0,0 +1,3 @@ +resources: + - apps.wandb.com_weightsandbiases.yaml + - apps.wandb.com_applications.yaml diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index e5b20412..57a562e9 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,14 +3,26 @@ # It should be run by config/default resources: - bases/apps.wandb.com_weightsandbiases.yaml +- bases/apps.wandb.com_applications.yaml # +kubebuilder:scaffold:crdkustomizeresource +# By default, both v1 and v2 are served. Uncomment one of the patches below to serve only one version: patches: +#- path: patches/use_v1_only.yaml +# target: +# kind: CustomResourceDefinition +# name: weightsandbiases.apps.wandb.com +#- path: patches/use_v2_only.yaml +# target: +# kind: CustomResourceDefinition +# name: weightsandbiases.apps.wandb.com + # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD +- path: patches/webhook_in_weightsandbiases.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [WEBHOOK] To enable webhook, uncomment the following section # the following config is for teaching kustomize how to do kustomization for CRDs. -#configurations: -#- kustomizeconfig.yaml +configurations: +- kustomizeconfig.yaml diff --git a/config/crd/patches/webhook_in_weightsandbiases.yaml b/config/crd/patches/webhook_in_weightsandbiases.yaml new file mode 100644 index 00000000..c5843bd4 --- /dev/null +++ b/config/crd/patches/webhook_in_weightsandbiases.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: weightsandbiases.apps.wandb.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 74377491..117051cf 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -20,9 +20,9 @@ resources: - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- ../webhook +- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -#- ../certmanager +- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus # [METRICS] Expose the controller manager metrics service. @@ -50,13 +50,13 @@ patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- path: manager_webhook_patch.yaml -# target: -# kind: Deployment +- path: manager_webhook_patch.yaml + target: + kind: Deployment # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # Uncomment the following replacements to add the cert-manager CA injection annotations -#replacements: +replacements: # - source: # Uncomment the following block to enable certificates for metrics # kind: Service # version: v1 @@ -95,118 +95,136 @@ patches: # index: 1 # create: true # -# - source: # Uncomment the following block if you have any webhook -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.name # Name of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 0 -# create: true -# - source: -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.namespace # Namespace of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 1 -# create: true + - source: # Uncomment the following block if you have any webhook + kind: Service + version: v1 + name: webhook-service + fieldPath: .metadata.name # Name of the service + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPaths: + - .spec.dnsNames.0 + - .spec.dnsNames.1 + options: + delimiter: '.' + index: 0 + create: true + - source: + kind: Service + version: v1 + name: webhook-service + fieldPath: .metadata.namespace # Namespace of the service + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPaths: + - .spec.dnsNames.0 + - .spec.dnsNames.1 + options: + delimiter: '.' + index: 1 + create: true # -# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # This name should match the one in certificate.yaml -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true + - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # This name should match the one in certificate.yaml + fieldPath: .metadata.namespace # Namespace of the certificate CR + targets: + - select: + kind: ValidatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true + - source: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPath: .metadata.name + targets: + - select: + kind: ValidatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true # -# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true + - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPath: .metadata.namespace # Namespace of the certificate CR + targets: + - select: + kind: MutatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true + - source: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPath: .metadata.name + targets: + - select: + kind: MutatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true # -# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. + - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPath: .metadata.namespace # Namespace of the certificate CR + targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. + - select: + kind: CustomResourceDefinition + name: weightsandbiases.apps.wandb.com + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true # +kubebuilder:scaffold:crdkustomizecainjectionns -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. + - source: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPath: .metadata.name + targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. + - select: + kind: CustomResourceDefinition + name: weightsandbiases.apps.wandb.com + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true # +kubebuilder:scaffold:crdkustomizecainjectionname diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml new file mode 100644 index 00000000..963c8a4c --- /dev/null +++ b/config/default/manager_webhook_patch.yaml @@ -0,0 +1,31 @@ +# This patch ensures the webhook certificates are properly mounted in the manager container. +# It configures the necessary arguments, volumes, volume mounts, and container ports. + +# Add the --webhook-cert-path argument for configuring the webhook certificate path +- op: add + path: /spec/template/spec/containers/0/args/- + value: --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs + +# Add the volumeMount for the webhook certificates +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + mountPath: /tmp/k8s-webhook-server/serving-certs + name: webhook-certs + readOnly: true + +# Add the port configuration for the webhook server +- op: add + path: /spec/template/spec/containers/0/ports/- + value: + containerPort: 9443 + name: webhook-server + protocol: TCP + +# Add the volume configuration for the webhook certificates +- op: add + path: /spec/template/spec/volumes/- + value: + name: webhook-certs + secret: + secretName: webhook-server-cert diff --git a/config/dev-common/delete-applications-crd.yaml b/config/dev-common/delete-applications-crd.yaml new file mode 100644 index 00000000..daac855b --- /dev/null +++ b/config/dev-common/delete-applications-crd.yaml @@ -0,0 +1,5 @@ +$patch: delete +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: applications.apps.wandb.com diff --git a/config/dev-common/delete-weightsandbiases-crd.yaml b/config/dev-common/delete-weightsandbiases-crd.yaml new file mode 100644 index 00000000..4ba5e687 --- /dev/null +++ b/config/dev-common/delete-weightsandbiases-crd.yaml @@ -0,0 +1,5 @@ +$patch: delete +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: weightsandbiases.apps.wandb.com diff --git a/config/dev-common/kustomization.yaml b/config/dev-common/kustomization.yaml new file mode 100644 index 00000000..409db598 --- /dev/null +++ b/config/dev-common/kustomization.yaml @@ -0,0 +1,16 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +patches: + - target: + group: apiextensions.k8s.io + version: v1 + kind: CustomResourceDefinition + name: applications.apps.wandb.com + path: delete-applications-crd.yaml + - target: + group: apiextensions.k8s.io + version: v1 + kind: CustomResourceDefinition + name: weightsandbiases.apps.wandb.com + path: delete-weightsandbiases-crd.yaml diff --git a/config/local-dev/README.md b/config/local-dev/README.md new file mode 100644 index 00000000..558c70f8 --- /dev/null +++ b/config/local-dev/README.md @@ -0,0 +1,227 @@ +# Local Webhook Development + +This directory contains configuration for running the operator with webhooks locally (outside the cluster) for development purposes. + +## Overview + +The standard webhook configuration expects the webhook server to run as a Service inside the cluster. For local development, this configuration patches the ValidatingWebhookConfiguration to use a URL pointing to your local machine instead. + +## Prerequisites + +- A Kubernetes cluster (kind, minikube, or similar) +- `kubectl` configured to access your cluster +- `openssl` for generating certificates +- The operator CRDs installed in your cluster + +## Quick Start + +### Option 1: Using Tilt (Recommended) + +If you're using Tilt for development: + +1. **Enable local webhook mode** in your `tilt-settings.star`: + ```python + SETTINGS = { + "localWebhookDev": True, + } + ``` + +2. **Start Tilt**: + ```bash + tilt up + ``` + +3. **In the Tilt UI, trigger the manual resource**: `Setup-Local-Webhook-Certs` + +4. **Run the controller** (outside Tilt, in your terminal or IDE): + ```bash + make run-local-webhook + ``` + +Tilt will: +- Apply CRDs, RBAC, and webhook configuration to your cluster +- Skip deploying the controller to the cluster +- Provide a manual button to setup webhook certificates +- Keep watching for code changes + +### Option 2: Manual Setup + +If you're not using Tilt: + +1. **Generate certificates and configure webhook**: + ```bash + ./scripts/setup-local-webhook.sh + ``` + +2. **Apply configuration to cluster**: + ```bash + kubectl apply -k config/local-dev + ``` + +3. **Run the controller locally**: + ```bash + go run ./cmd/controller/main.go \ + --webhook-cert-path=.local-webhook-certs \ + --webhook-cert-name=tls.crt \ + --webhook-cert-key=tls.key \ + --v2-webhook=true + ``` + + Or use the Makefile target: + ```bash + make run-local-webhook + ``` + +The webhook server will start on `https://host.docker.internal:9443` (or your configured host). + +## How It Works + +The `setup-local-webhook.sh` script automatically detects your environment: + +1. **Checks for kind cluster**: Uses the same cluster name resolution as `setup_kind.sh` (reads from `tilt-settings.star` if available) +2. **Detects platform**: macOS, Linux, Docker Desktop, or OrbStack +3. **Configures appropriate host**: Uses the Docker bridge gateway for kind, or `host.docker.internal` for Docker Desktop/OrbStack +4. **Generates certificates**: Creates self-signed TLS certificates with the correct hostname +5. **Updates webhook config**: Patches the ValidatingWebhookConfiguration with the URL and CA bundle + +### Most Common Usage + +For most setups, just run: + +```bash +./scripts/setup-local-webhook.sh +``` + +The script will detect your environment and configure everything automatically. + +## Platform-Specific Configuration + +### macOS (Docker Desktop or OrbStack) + +Auto-detection works out of the box: + +```bash +./scripts/setup-local-webhook.sh +``` + +The script detects: +- **OrbStack**: Uses `host.docker.internal` (natively supported) +- **Docker Desktop**: Uses `host.docker.internal` +- **kind cluster**: Automatically uses the Docker bridge gateway IP + +### Linux + +Auto-detection works for most setups: + +```bash +./scripts/setup-local-webhook.sh +``` + +The script automatically: +- Detects if you're using kind and finds the Docker bridge gateway IP +- Falls back to detecting the default Docker bridge IP +- Uses sensible defaults (172.17.0.1) if auto-detection fails + +**Manual override** (if auto-detection doesn't work): + +```bash +# Find your Docker bridge IP +docker network inspect kind | grep Gateway +# Usually 172.18.0.1 or 172.17.0.1 + +# Set the webhook host before running setup +export WEBHOOK_HOST=172.18.0.1 +./scripts/setup-local-webhook.sh +``` + +### Minikube + +For minikube, use the host IP that the minikube VM can reach: + +```bash +# Get the host IP (usually 192.168.x.x) +export WEBHOOK_HOST=$(minikube ssh -- ip route | grep default | awk '{print $3}') +./scripts/setup-local-webhook.sh +``` + +## Customizing the Port + +By default, the webhook server runs on port 9443. To use a different port: + +```bash +export WEBHOOK_PORT=8443 +./scripts/setup-local-webhook.sh +``` + +Then run the controller with the matching port configuration. + +## Disabling the Webhook + +If you want to develop without the webhook: + +```bash +go run ./cmd/controller/main.go --v2-webhook=false +``` + +Or set the environment variable: + +```bash +export V2_WEBHOOK=false +go run ./cmd/controller/main.go +``` + +## Troubleshooting + +### "Connection refused" errors + +The Kubernetes API server cannot reach your local webhook. Check: +- The webhook host is correct for your platform +- Your firewall allows connections on port 9443 +- The controller is running and listening on the correct port + +### "x509: certificate is valid for X, not Y" + +The certificate doesn't match the hostname. Regenerate certificates with the correct host: + +```bash +export WEBHOOK_HOST=<your-correct-host> +./scripts/setup-local-webhook.sh +kubectl apply -k config/local-dev +``` + +### "context deadline exceeded" + +The webhook took too long to respond. This usually means: +- The controller crashed or isn't running +- The webhook validation logic has an infinite loop or deadlock +- Network connectivity issues + +Check the controller logs for errors. + +## Files Generated + +- `.local-webhook-certs/ca.crt` - CA certificate +- `.local-webhook-certs/ca.key` - CA private key +- `.local-webhook-certs/tls.crt` - Server certificate +- `.local-webhook-certs/tls.key` - Server private key +- `config/local-dev/webhook_local_patch.yaml` - Updated with CA bundle + +## Cleaning Up + +To remove the local development configuration: + +```bash +kubectl delete -k config/local-dev +rm -rf .local-webhook-certs +``` + +## Differences from Production + +This local development setup differs from production in these ways: + +1. **URL instead of Service**: Uses a URL pointing to localhost instead of an in-cluster Service +2. **Self-signed certificates**: Uses locally generated certificates instead of cert-manager +3. **No cert-manager**: Certificate management is manual +4. **Direct access**: The Kubernetes API server connects directly to your machine + +For production deployment, use the standard configuration with cert-manager or a proper certificate authority. \ No newline at end of file diff --git a/config/local-dev/kustomization.yaml b/config/local-dev/kustomization.yaml new file mode 100644 index 00000000..24dc02b8 --- /dev/null +++ b/config/local-dev/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../crd +- ../webhook + +namespace: operator-system +namePrefix: operator- + +patches: +- path: webhook_local_patch.yaml \ No newline at end of file diff --git a/config/local-dev/webhook_local_patch.yaml b/config/local-dev/webhook_local_patch.yaml new file mode 100644 index 00000000..33bb133d --- /dev/null +++ b/config/local-dev/webhook_local_patch.yaml @@ -0,0 +1,22 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration +webhooks: +- name: mweightsandbiases.wandb.com + clientConfig: + $patch: replace + url: https://host.docker.internal:9443/mutate-apps-wandb-com-v2-weightsandbiases + caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURGekNDQWYrZ0F3SUJBZ0lVUmUwdjVuYlhGSHluRjBpU3JrVUlndkN5RTZFd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0d6RVpNQmNHQTFVRUF3d1FURzlqWVd3Z1YyVmlhRzl2YXlCRFFUQWVGdzB5TlRFeE1EVXhPVEk1TXpaYQpGdzB6TlRFeE1ETXhPVEk1TXpaYU1Cc3hHVEFYQmdOVkJBTU1FRXh2WTJGc0lGZGxZbWh2YjJzZ1EwRXdnZ0VpCk1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ2lIV3EzSDJKZTBjUW5sYUdvNmZrRjJoVzMKbGdla2xweWhneExrV1V0S1RmdXc2aHAwaXhqRUtxYkR4RitmYVJRNzJ4bU9EdGp1cDJvYmhDbEZ0a1o0SkExTgpEVVdJcWdBN0dtbWNnYmdnRUcvbmUxVVRNNTB4YlZzSzZzZEFTSlZ0R2dFbm9GWE9NZFhCbFFmQTZlMEE4emxhCi9lVkNDckd3RHQvZldoWEEvNTNEbTJtTjZ6QWxPV0djVHZhMUFLRU9PZm85Mm55NFlUUmx2Q0FkWkoxMTVXNjgKb0JBdzZaRkU1aTRYVEpsRHpWWlltTm9kelNyQ0FxSCtiaWJ3eE1jQWE1ay9ockQvcVNCbFk4Mmk0dXlMbWkzMQpTYVNITyt4ZU9MMkkxOUhWNGMzZFV3MjhKREhxSEhiSkRRL1o4U3ZaT2RxN0xiTTNoNWZlYzJFaVVFMEpBZ01CCkFBR2pVekJSTUIwR0ExVWREZ1FXQkJRZUlUOXAxRXovNmg2allUMmtEcW5xM3dReUlqQWZCZ05WSFNNRUdEQVcKZ0JRZUlUOXAxRXovNmg2allUMmtEcW5xM3dReUlqQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BMEdDU3FHU0liMwpEUUVCQ3dVQUE0SUJBUUJ0eFo2WVFheXljUVdKK0haWWNDU3lvSyt1ZlZuUSs0aE0yWHV5cVpWT0NDTXNGc2hICnViQi9lZEpadHh2aHdNejhYQ2V1K0dralVOc1hrQVhWejY1c0w2YjJhUVkxWFFjYXcrWG55VkN2YW5jTjU4N2sKOEsxQkxDOW1lazh4YVU5UVg5QllFN3pVQTArK1hWSVpFSUM2OHNmcS85Z25OSU9TRHk0dHJOS0FrbjN4Vlk4UApMamNpQ1lScUhNWTZwRWF2aFpsbWRIcldLODdNRHlmaDhuTjMyd29GLzZ1bHh2V0NZYW1OdHRvNjFISFVaVU0xCkRwZG1iSzR5VWR1VitKRmt2cHNuMFA5TkVsZlltWGd6QW1jQXBTQUtEUkhybk4vZjBuc29jUVBSckNTMjBhUU8KenFidHJrRVZWZS9MaEFsQVpISWpTb2cwdDgrQ2NRYXFoVUVZCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- name: vweightsandbiases.wandb.com + clientConfig: + $patch: replace + url: https://host.docker.internal:9443/validate-apps-wandb-com-v2-weightsandbiases + caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURGekNDQWYrZ0F3SUJBZ0lVUmUwdjVuYlhGSHluRjBpU3JrVUlndkN5RTZFd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0d6RVpNQmNHQTFVRUF3d1FURzlqWVd3Z1YyVmlhRzl2YXlCRFFUQWVGdzB5TlRFeE1EVXhPVEk1TXpaYQpGdzB6TlRFeE1ETXhPVEk1TXpaYU1Cc3hHVEFYQmdOVkJBTU1FRXh2WTJGc0lGZGxZbWh2YjJzZ1EwRXdnZ0VpCk1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ2lIV3EzSDJKZTBjUW5sYUdvNmZrRjJoVzMKbGdla2xweWhneExrV1V0S1RmdXc2aHAwaXhqRUtxYkR4RitmYVJRNzJ4bU9EdGp1cDJvYmhDbEZ0a1o0SkExTgpEVVdJcWdBN0dtbWNnYmdnRUcvbmUxVVRNNTB4YlZzSzZzZEFTSlZ0R2dFbm9GWE9NZFhCbFFmQTZlMEE4emxhCi9lVkNDckd3RHQvZldoWEEvNTNEbTJtTjZ6QWxPV0djVHZhMUFLRU9PZm85Mm55NFlUUmx2Q0FkWkoxMTVXNjgKb0JBdzZaRkU1aTRYVEpsRHpWWlltTm9kelNyQ0FxSCtiaWJ3eE1jQWE1ay9ockQvcVNCbFk4Mmk0dXlMbWkzMQpTYVNITyt4ZU9MMkkxOUhWNGMzZFV3MjhKREhxSEhiSkRRL1o4U3ZaT2RxN0xiTTNoNWZlYzJFaVVFMEpBZ01CCkFBR2pVekJSTUIwR0ExVWREZ1FXQkJRZUlUOXAxRXovNmg2allUMmtEcW5xM3dReUlqQWZCZ05WSFNNRUdEQVcKZ0JRZUlUOXAxRXovNmg2allUMmtEcW5xM3dReUlqQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BMEdDU3FHU0liMwpEUUVCQ3dVQUE0SUJBUUJ0eFo2WVFheXljUVdKK0haWWNDU3lvSyt1ZlZuUSs0aE0yWHV5cVpWT0NDTXNGc2hICnViQi9lZEpadHh2aHdNejhYQ2V1K0dralVOc1hrQVhWejY1c0w2YjJhUVkxWFFjYXcrWG55VkN2YW5jTjU4N2sKOEsxQkxDOW1lazh4YVU5UVg5QllFN3pVQTArK1hWSVpFSUM2OHNmcS85Z25OSU9TRHk0dHJOS0FrbjN4Vlk4UApMamNpQ1lScUhNWTZwRWF2aFpsbWRIcldLODdNRHlmaDhuTjMyd29GLzZ1bHh2V0NZYW1OdHRvNjFISFVaVU0xCkRwZG1iSzR5VWR1VitKRmt2cHNuMFA5TkVsZlltWGd6QW1jQXBTQUtEUkhybk4vZjBuc29jUVBSckNTMjBhUU8KenFidHJrRVZWZS9MaEFsQVpISWpTb2cwdDgrQ2NRYXFoVUVZCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index c3ffd232..813c8077 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -65,7 +65,15 @@ spec: - --health-probe-bind-address=:8081 image: controller:latest name: manager - ports: [] + ports: + - containerPort: 8443 + name: metrics + protocol: TCP + env: + - name: METRICS_BIND_ADDRESS + value: ":8443" + - name: METRICS_SECURE + value: "false" securityContext: allowPrivilegeEscalation: false capabilities: diff --git a/config/network-policy/allow-webhook-traffic.yaml b/config/network-policy/allow-webhook-traffic.yaml new file mode 100644 index 00000000..08fbd901 --- /dev/null +++ b/config/network-policy/allow-webhook-traffic.yaml @@ -0,0 +1,27 @@ +# This NetworkPolicy allows ingress traffic to your webhook server running +# as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks +# will only work when applied in namespaces labeled with 'webhook: enabled' +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: allow-webhook-traffic + namespace: system +spec: + podSelector: + matchLabels: + control-plane: controller-manager + app.kubernetes.io/name: operator + policyTypes: + - Ingress + ingress: + # This allows ingress traffic from any namespace with the label webhook: enabled + - from: + - namespaceSelector: + matchLabels: + webhook: enabled # Only from namespaces with this label + ports: + - port: 443 + protocol: TCP diff --git a/config/network-policy/kustomization.yaml b/config/network-policy/kustomization.yaml index ec0fb5e5..0872bee1 100644 --- a/config/network-policy/kustomization.yaml +++ b/config/network-policy/kustomization.yaml @@ -1,2 +1,3 @@ resources: +- allow-webhook-traffic.yaml - allow-metrics-traffic.yaml diff --git a/config/openshift-dev/README.md b/config/openshift-dev/README.md new file mode 100644 index 00000000..af5c68a9 --- /dev/null +++ b/config/openshift-dev/README.md @@ -0,0 +1,104 @@ +# OpenShift Local Development with CRC + +Tilt configures the local operator deployment for OpenShift's `restricted-v2` +SCC when targeting a CRC (Red Hat OpenShift Local) cluster. + +## Prerequisites + +- [CRC](https://console.redhat.com/openshift/create/local) (free Red Hat account required) +- [oc CLI](https://formulae.brew.sh/formula/openshift-cli): `brew install openshift-cli` +- [Tilt](https://docs.tilt.dev/install.html): `brew install tilt-dev/tap/tilt` +- Docker (via OrbStack or Docker Desktop) +- At least 16GB RAM allocated to CRC (48GB+ host recommended) + +## Setup + +### 1. Install CRC + +```bash +crc setup +``` + +This downloads a ~4GB VM image on first run and prompts for a pull secret (available from the CRC download page). The setup script handles configuration and starting CRC. + +### 2. Run the setup script + +```bash +./hack/scripts/setup_crc.sh +``` + +This script: +- Configures CRC memory (16GB) and disk size (80GB) +- Starts CRC if not already running +- Logs in as `kubeadmin` +- Exposes the internal image registry via a route +- Configures Docker to trust the CRC registry (handles OrbStack automatically) +- Creates the local operator and W&B namespaces + +### 3. Configure tilt-settings.star + +```python +SETTINGS = { + "allowedContexts": ["crc-admin"], +} +``` + +### 4. Start Tilt + +```bash +kubectl config use-context crc-admin +tilt up +``` + +## How it works + +When Tilt detects a CRC context, it automatically: + +- Enables `openshiftSCC` mode +- Applies `deploy/operator/profiles/openshift.yaml` to the operator Helm release +- Builds the operator image with a non-root, group-writable `/helm` directory +- Removes fixed pod UID/GID/fsGroup settings from the local operator deployment +- Sets `OPENSHIFT=true` so managed dependency specs omit fixed IDs and satisfy OpenShift SCCs +- Pushes images to CRC's internal registry (`default-route-openshift-image-registry.apps-crc.testing`) + +### Security settings + +Tilt applies local operator Helm values that satisfy the `restricted-v2` SCC: + +- `runAsNonRoot: true` +- `seccompProfile: RuntimeDefault` +- `allowPrivilegeEscalation: false` +- `readOnlyRootFilesystem: true` +- `capabilities.drop: [ALL]` + +OpenShift assigns the UID from the namespace's `openshift.io/sa.scc.uid-range` annotation at admission time. + +## Troubleshooting + +### Docker push fails with TLS certificate error + +The setup script handles this automatically for OrbStack. If using Docker Desktop, add the CRC registry to **Settings > Docker Engine > insecure-registries**: + +```json +{ + "insecure-registries": ["default-route-openshift-image-registry.apps-crc.testing"] +} +``` + +### Pods stuck in Pending (insufficient memory) + +The setup script configures 16GB RAM automatically. If CRC was already running when the script ran, the config won't take effect until restart: + +```bash +crc stop +./hack/scripts/setup_crc.sh +``` + +### Docker login expired + +CRC tokens expire. Re-run the setup script or manually: + +```bash +oc login -u kubeadmin -p $(crc console --credentials | grep kubeadmin | sed 's/.*-p \([^ ]*\) .*/\1/') https://api.crc.testing:6443 +oc whoami -t | docker login -u kubeadmin --password-stdin default-route-openshift-image-registry.apps-crc.testing +``` diff --git a/config/openshift-dev/kustomization.yaml b/config/openshift-dev/kustomization.yaml new file mode 100644 index 00000000..dd848fd3 --- /dev/null +++ b/config/openshift-dev/kustomization.yaml @@ -0,0 +1,13 @@ +resources: + - ../default + +components: + - ../dev-common + +patches: + - target: + group: apps + version: v1 + kind: Deployment + name: controller-manager + path: openshift-security.yaml diff --git a/config/openshift-dev/openshift-security.yaml b/config/openshift-dev/openshift-security.yaml new file mode 100644 index 00000000..d2d93ae3 --- /dev/null +++ b/config/openshift-dev/openshift-security.yaml @@ -0,0 +1,19 @@ +- op: replace + path: /spec/template/spec/securityContext + value: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault +- op: replace + path: /spec/template/spec/containers/0/securityContext + value: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: KAFKA_FSGROUP + value: "0" diff --git a/config/rbac/application_admin_role.yaml b/config/rbac/application_admin_role.yaml new file mode 100644 index 00000000..5b88b504 --- /dev/null +++ b/config/rbac/application_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over apps.wandb.com. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: application-admin-role +rules: +- apiGroups: + - apps.wandb.com + resources: + - applications + verbs: + - '*' +- apiGroups: + - apps.wandb.com + resources: + - applications/status + verbs: + - get diff --git a/config/rbac/application_editor_role.yaml b/config/rbac/application_editor_role.yaml new file mode 100644 index 00000000..2c0aff6a --- /dev/null +++ b/config/rbac/application_editor_role.yaml @@ -0,0 +1,33 @@ +# This rule is not used by the project operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the apps.wandb.com. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: application-editor-role +rules: +- apiGroups: + - apps.wandb.com + resources: + - applications + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps.wandb.com + resources: + - applications/status + verbs: + - get diff --git a/config/rbac/application_viewer_role.yaml b/config/rbac/application_viewer_role.yaml new file mode 100644 index 00000000..c01499da --- /dev/null +++ b/config/rbac/application_viewer_role.yaml @@ -0,0 +1,29 @@ +# This rule is not used by the project operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to apps.wandb.com resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: application-viewer-role +rules: +- apiGroups: + - apps.wandb.com + resources: + - applications + verbs: + - get + - list + - watch +- apiGroups: + - apps.wandb.com + resources: + - applications/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 6409b967..35662c43 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -22,6 +22,9 @@ resources: # default, aiding admins in cluster management. Those roles are # not used by the {{ .ProjectName }} itself. You can comment the following lines # if you do not want those helpers be installed with your Project. +- application_admin_role.yaml +- application_editor_role.yaml +- application_viewer_role.yaml - weightsandbiases_admin_role.yaml - weightsandbiases_editor_role.yaml - weightsandbiases_viewer_role.yaml diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 8a96d0ae..22dac7bc 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -29,7 +29,6 @@ rules: - "" resources: - endpoints - - ingresses - namespaces - namespaces/status - nodes @@ -75,6 +74,7 @@ rules: - apiGroups: - apps.wandb.com resources: + - applications - weightsandbiases verbs: - create @@ -87,12 +87,14 @@ rules: - apiGroups: - apps.wandb.com resources: + - applications/finalizers - weightsandbiases/finalizers verbs: - update - apiGroups: - apps.wandb.com resources: + - applications/status - weightsandbiases/status verbs: - get @@ -121,7 +123,44 @@ rules: - get - list - patch + - update + - watch +- apiGroups: + - clickhouse-keeper.altinity.com + resources: + - clickhousekeeperinstallations + verbs: + - create + - delete + - get + - list + - patch + - update - watch +- apiGroups: + - clickhouse-keeper.altinity.com + resources: + - clickhousekeeperinstallations/status + verbs: + - get +- apiGroups: + - clickhouse.altinity.com + resources: + - clickhouseinstallations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - clickhouse.altinity.com + resources: + - clickhouseinstallations/status + verbs: + - get - apiGroups: - cloud.google.com resources: @@ -153,11 +192,94 @@ rules: - get - list - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - backendtlspolicies + - gateways + - httproutes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - backendtlspolicies/status + - gateways/status + - httproutes/status + verbs: + - get +- apiGroups: + - gateway.nginx.org + resources: + - clientsettingspolicies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - grafana.integreatly.org + resources: + - grafanadashboards + - grafanadatasources + - grafanas + verbs: + - get + - list + - watch +- apiGroups: + - grafana.integreatly.org + resources: + - grafanadashboards/status + - grafanadatasources/status + - grafanas/status + verbs: + - get +- apiGroups: + - moco.cybozu.com + resources: + - mysqlclusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - moco.cybozu.com + resources: + - mysqlclusters/status + verbs: + - get + - patch + - update +- apiGroups: + - networking.gke.io + resources: + - healthcheckpolicies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - networking.k8s.io resources: - ingresses - - ingresses/status - networkpolicies verbs: - create @@ -167,6 +289,40 @@ rules: - patch - update - watch +- apiGroups: + - networking.k8s.io + resources: + - ingresses/status + verbs: + - get + - list + - watch +- apiGroups: + - operator.victoriametrics.com + resources: + - vlsingles + - vmagents + - vmalerts + - vmnodescrapes + - vmpodscrapes + - vmrules + - vmservicescrapes + - vmsingles + - vtsingles + verbs: + - get + - list + - watch +- apiGroups: + - operator.victoriametrics.com + resources: + - vlsingles/status + - vmagents/status + - vmalerts/status + - vmsingles/status + - vtsingles/status + verbs: + - get - apiGroups: - policy resources: @@ -194,3 +350,49 @@ rules: - patch - update - watch +- apiGroups: + - redis.redis.opstreelabs.in + resources: + - redis + - redisreplications + - redissentinels + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - redis.redis.opstreelabs.in + resources: + - redis/status + verbs: + - get +- apiGroups: + - seaweed.seaweedfs.com + resources: + - seaweeds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - seaweed.seaweedfs.com + resources: + - seaweeds/status + verbs: + - get +- apiGroups: + - security.openshift.io + resourceNames: + - nonroot-v2 + resources: + - securitycontextconstraints + verbs: + - use diff --git a/config/samples/apps_v2_application.yaml b/config/samples/apps_v2_application.yaml new file mode 100644 index 00000000..71714c26 --- /dev/null +++ b/config/samples/apps_v2_application.yaml @@ -0,0 +1,45 @@ +apiVersion: apps.wandb.com/v2 +kind: Application +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: application-sample +spec: + kind: Deployment + podTemplate: + metadata: + annotations: + foo: bar + spec: + containers: + - name: my-app + image: nginx:1.14.2 + + jobs: + - metadata: + name: my-job + spec: + template: + spec: + restartPolicy: Never + containers: + - name: my-job + image: nginx:1.14.1 + command: ["sleep", "10"] + + cronJobs: + - metadata: + name: my-cronjob + spec: + schedule: "*/1 * * * *" + jobTemplate: + spec: + template: + spec: + restartPolicy: Never + containers: + - name: my-cronjob + image: nginx:1.14.2 + command: ["sleep", "10"] + diff --git a/config/samples/apps_v2_weightsandbiases.yaml b/config/samples/apps_v2_weightsandbiases.yaml new file mode 100644 index 00000000..121c1ed1 --- /dev/null +++ b/config/samples/apps_v2_weightsandbiases.yaml @@ -0,0 +1,24 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb-dev-v2 +spec: + size: dev + mysql: + managedMysql: + storageSize: 5Gi + redis: + managedRedis: + storageSize: 1Gi + kafka: + managedKafka: + storageSize: 10Gi + replicas: 1 + objectStore: + managedObjectStore: + storageSize: 10Gi + replicas: 1 + clickhouse: + managedClickhouse: + storageSize: 10Gi + replicas: 1 diff --git a/config/samples/kafka-operator-configmap.yaml b/config/samples/kafka-operator-configmap.yaml new file mode 100644 index 00000000..1cf27113 --- /dev/null +++ b/config/samples/kafka-operator-configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: kafka-operator-templates + namespace: wandb-system + labels: + app.kubernetes.io/name: wandb-operator + app.kubernetes.io/component: kafka-templates +data: + # This ConfigMap is generated using kustomize from the kafka-templates directory + # To regenerate: kubectl kustomize config/kafka-templates > config/samples/kafka-operator-configmap.yaml + # Then you need to manually extract just the data section or use the full generated ConfigMap \ No newline at end of file diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 7357860d..12dd6d2f 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -1,4 +1,6 @@ ## Append samples of your project ## resources: - apps_v1_weightsandbiases.yaml +- apps_v2_weightsandbiases.yaml +- apps_v2_application.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/tilt-dev/kustomization.yaml b/config/tilt-dev/kustomization.yaml new file mode 100644 index 00000000..24b101a3 --- /dev/null +++ b/config/tilt-dev/kustomization.yaml @@ -0,0 +1,13 @@ +resources: + - ../default + +components: + - ../dev-common + +patches: + - target: + group: apps + version: v1 + kind: Deployment + name: controller-manager + path: security.yaml diff --git a/config/tilt-dev/security.yaml b/config/tilt-dev/security.yaml new file mode 100644 index 00000000..0e22a570 --- /dev/null +++ b/config/tilt-dev/security.yaml @@ -0,0 +1,9 @@ +- op: replace + path: /spec/template/spec/securityContext/runAsNonRoot + value: false +- op: replace + path: /spec/template/spec/containers/0/securityContext/allowPrivilegeEscalation + value: true +- op: replace + path: /spec/template/spec/containers/0/securityContext/capabilities/drop + value: [] \ No newline at end of file diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 00000000..739aeb55 --- /dev/null +++ b/config/webhook/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- manifests.yaml +- service.yaml + +configurations: + - kustomizeconfig.yaml diff --git a/config/webhook/kustomizeconfig.yaml b/config/webhook/kustomizeconfig.yaml new file mode 100644 index 00000000..206316e5 --- /dev/null +++ b/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,22 @@ +# the following config is for teaching kustomize where to look at when substituting nameReference. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml new file mode 100644 index 00000000..c985e5fe --- /dev/null +++ b/config/webhook/manifests.yaml @@ -0,0 +1,92 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-apps-wandb-com-v2-application + failurePolicy: Fail + name: mapplication-v2.kb.io + rules: + - apiGroups: + - apps.wandb.com + apiVersions: + - v2 + operations: + - CREATE + - UPDATE + resources: + - applications + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-apps-wandb-com-v2-weightsandbiases + failurePolicy: Fail + name: mweightsandbiases-v2.kb.io + rules: + - apiGroups: + - apps.wandb.com + apiVersions: + - v2 + operations: + - CREATE + - UPDATE + resources: + - weightsandbiases + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-apps-wandb-com-v2-application + failurePolicy: Fail + name: vapplication-v2.kb.io + rules: + - apiGroups: + - apps.wandb.com + apiVersions: + - v2 + operations: + - CREATE + - UPDATE + resources: + - applications + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-apps-wandb-com-v2-weightsandbiases + failurePolicy: Fail + name: vweightsandbiases-v2.kb.io + rules: + - apiGroups: + - apps.wandb.com + apiVersions: + - v2 + operations: + - CREATE + - UPDATE + resources: + - weightsandbiases + sideEffects: None diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 00000000..a10cc235 --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: operator + app.kubernetes.io/managed-by: kustomize + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager + app.kubernetes.io/name: operator diff --git a/deploy/.yamllint.yaml b/deploy/.yamllint.yaml new file mode 100644 index 00000000..95a858b2 --- /dev/null +++ b/deploy/.yamllint.yaml @@ -0,0 +1,11 @@ +extends: default + +rules: + new-line-at-end-of-file: enable + document-end: disable + line-length: disable + trailing-spaces: enable + braces: disable + indentation: disable + document-start: disable + hyphens: disable diff --git a/deploy/ct.yaml b/deploy/ct.yaml new file mode 100644 index 00000000..0a254780 --- /dev/null +++ b/deploy/ct.yaml @@ -0,0 +1,15 @@ +# See https://github.com/helm/chart-testing#configuration +remote: origin +target-branch: main +chart-dirs: + - deploy +chart-repos: + - altinity=https://helm.altinity.com + - grafana=https://grafana.github.io/helm-charts/ + - moco=https://cybozu-go.github.io/moco/ + - redis=https://ot-container-kit.github.io/helm-charts/ + - victoria-metrics=https://victoriametrics.github.io/helm-charts/ + - prometheus-community=https://prometheus-community.github.io/helm-charts/ + - seaweedfs=https://seaweedfs.github.io/seaweedfs-operator/ + - wandb=https://charts.wandb.ai +yamllint-config: .yamllint.yaml diff --git a/deploy/operator/Chart.lock b/deploy/operator/Chart.lock new file mode 100644 index 00000000..5a1c1012 --- /dev/null +++ b/deploy/operator/Chart.lock @@ -0,0 +1,30 @@ +dependencies: +- name: wandb-base + repository: https://charts.wandb.ai/ + version: 0.11.8 +- name: moco + repository: https://cybozu-go.github.io/moco/ + version: 0.26.0 +- name: redis-operator + repository: https://ot-container-kit.github.io/helm-charts + version: 0.22.2 +- name: seaweedfs-operator + repository: https://seaweedfs.github.io/seaweedfs-operator/ + version: 0.1.24 +- name: prometheus-operator-crds + repository: https://prometheus-community.github.io/helm-charts + version: 29.0.0 +- name: altinity-clickhouse-operator + repository: https://helm.altinity.com + version: 0.26.3 +- name: victoria-metrics-operator + repository: https://victoriametrics.github.io/helm-charts/ + version: 0.58.1 +- name: grafana-operator + repository: https://grafana.github.io/helm-charts + version: 5.21.4 +- name: telemetry + repository: file://../telemetry + version: 0.1.0 +digest: sha256:c236896848dcfa23f87e476e0ea552ffb874e294e08e614a7908019e03ee68a1 +generated: "2026-07-08T16:05:34.522143-07:00" diff --git a/deploy/operator/Chart.yaml b/deploy/operator/Chart.yaml new file mode 100644 index 00000000..f280ab02 --- /dev/null +++ b/deploy/operator/Chart.yaml @@ -0,0 +1,48 @@ +apiVersion: v2 +name: operator +description: A Helm chart for Weights & Biases operator +type: application +version: 2.0.0-alpha.2 +appVersion: "2.0.0-alpha.2" +maintainers: + - name: wandb + email: support@wandb.com + url: https://wandb.com + +dependencies: + - name: wandb-base + version: 0.11.8 + alias: wandb-operator + repository: https://charts.wandb.ai/ + condition: wandb-operator.enabled + - name: moco + version: 0.26.0 + repository: https://cybozu-go.github.io/moco/ + condition: moco.enabled + - name: redis-operator + version: 0.22.2 + repository: https://ot-container-kit.github.io/helm-charts + condition: redis-operator.enabled + - name: seaweedfs-operator + version: 0.1.24 + repository: https://seaweedfs.github.io/seaweedfs-operator/ + condition: seaweedfs-operator.enabled + - name: prometheus-operator-crds + version: 29.0.0 + repository: https://prometheus-community.github.io/helm-charts + condition: seaweedfs-operator.enabled + - name: altinity-clickhouse-operator + version: 0.26.3 + repository: https://helm.altinity.com + condition: altinity-clickhouse-operator.enabled + - name: victoria-metrics-operator + version: 0.58.1 + repository: https://victoriametrics.github.io/helm-charts/ + condition: victoria-metrics-operator.enabled + - name: grafana-operator + version: 5.21.4 + repository: https://grafana.github.io/helm-charts + condition: grafana-operator.enabled + - name: telemetry + version: 0.1.0 + repository: file://../telemetry diff --git a/deploy/operator/profiles/openshift.yaml b/deploy/operator/profiles/openshift.yaml new file mode 100644 index 00000000..3b289340 --- /dev/null +++ b/deploy/operator/profiles/openshift.yaml @@ -0,0 +1,44 @@ +openshift: + enabled: true + +wandb-operator: + podSecurityContext: + runAsUser: null + runAsGroup: null + fsGroup: null + fsGroupChangePolicy: null + + containers: + operator: + env: + OPENSHIFT: + value: "true" + +# Null these operators' hardcoded IDs so restricted-v2 assigns valid ones. +redis-operator: + podSecurityContext: + runAsUser: null + runAsGroup: null + fsGroup: null + fsGroupChangePolicy: null + +altinity-clickhouse-operator: + podSecurityContext: + runAsUser: null + runAsGroup: null + fsGroup: null + fsGroupChangePolicy: null + +seaweedfs-operator: + podSecurityContext: + runAsUser: null + runAsGroup: null + fsGroup: null + +# Disable moco's injected fixed 10000 IDs so restricted-v2 admits MySQL pods. +moco: + extraArgs: + - --disable-default-security-context + +grafana-operator: + isOpenShift: true diff --git a/deploy/operator/profiles/telemetry-forward.yaml b/deploy/operator/profiles/telemetry-forward.yaml new file mode 100644 index 00000000..8eb03cba --- /dev/null +++ b/deploy/operator/profiles/telemetry-forward.yaml @@ -0,0 +1,8 @@ +telemetry: + mode: "forward" + +victoria-metrics-operator: + enabled: true + +grafana-operator: + enabled: false diff --git a/deploy/operator/profiles/telemetry-full.yaml b/deploy/operator/profiles/telemetry-full.yaml new file mode 100644 index 00000000..0df63d81 --- /dev/null +++ b/deploy/operator/profiles/telemetry-full.yaml @@ -0,0 +1,10 @@ +telemetry: + mode: "full" + scrape: + kubeStateMetrics: true + +victoria-metrics-operator: + enabled: true + +grafana-operator: + enabled: true diff --git a/deploy/operator/profiles/telemetry-off.yaml b/deploy/operator/profiles/telemetry-off.yaml new file mode 100644 index 00000000..c6352af8 --- /dev/null +++ b/deploy/operator/profiles/telemetry-off.yaml @@ -0,0 +1,8 @@ +telemetry: + mode: "off" + +victoria-metrics-operator: + enabled: false + +grafana-operator: + enabled: false diff --git a/deploy/operator/templates/_helpers.tpl b/deploy/operator/templates/_helpers.tpl new file mode 100644 index 00000000..8c1e63e5 --- /dev/null +++ b/deploy/operator/templates/_helpers.tpl @@ -0,0 +1,39 @@ +{{/* +Name of the CRD installer hook resources (ServiceAccount, Role, Job). +*/}} +{{- define "wandb-operator.crdInstallerName" -}} +{{- printf "%s-crd-installer" .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Comma-separated list of optional CRD groups for the crd-installer's --groups flag. +Drives which embedded CRDs the installer applies; operator-owned CRDs are always +installed regardless. Add new groups here as new subchart-CRD dependencies land. +*/}} +{{- define "wandb-operator.crdGroups" -}} +{{- $groups := list -}} +{{- if (dig "redis-operator" "enabled" false .Values.AsMap) -}} + {{- $groups = append $groups "redis" -}} +{{- end -}} +{{- if (dig "altinity-clickhouse-operator" "enabled" false .Values.AsMap) -}} + {{- $groups = append $groups "clickhouse" -}} +{{- end -}} +{{- join "," $groups -}} +{{- end -}} + +{{/* +Webhook conversion service name. Mirrors the conditional that used to live +inside operator-crds/crds.yaml: honor an explicit override on +.Values.wandb-operator.service.name, else fall back to "<release>-wandb-operator". +*/}} +{{- define "wandb-operator.webhookServiceName" -}} +{{ include "wandb-operator.serviceName" . }} +{{- end -}} + +{{- define "wandb-operator.fullname" -}} +{{ include "wandb-base.fullname" (dict "Release" (dict "Name" .Release.Name) "Chart" (dict "Name" "wandb-operator") "Values" (dict "nameOverride" (dig "wandb-operator" "nameOverride" "" .Values.AsMap))) }} +{{- end }} + +{{- define "wandb-operator.serviceName" -}} +{{ include "wandb-base.serviceName" (dict "Release" (dict "Name" .Release.Name) "Chart" (dict "Name" "wandb-operator") "Values" (dict "nameOverride" (dig "wandb-operator" "nameOverride" "" .Values.AsMap) "service" (dict "name" (dig "wandb-operator" "service" "name" "" .Values.AsMap)))) }} +{{- end }} diff --git a/deploy/operator/templates/certificates.yaml b/deploy/operator/templates/certificates.yaml new file mode 100644 index 00000000..f105004f --- /dev/null +++ b/deploy/operator/templates/certificates.yaml @@ -0,0 +1,24 @@ +{{- if not (eq (index .Values "wandb-operator" "enabled") false) }} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ .Release.Name }}-serving-cert + namespace: {{ .Release.Namespace }} +spec: + dnsNames: + - {{ include "wandb-operator.serviceName" . }}.{{ .Release.Namespace }}.svc + - {{ include "wandb-operator.serviceName" . }}.{{ .Release.Namespace }}.svc.cluster.local + issuerRef: + kind: Issuer + name: {{ .Release.Name }}-selfsigned-issuer + secretName: {{ .Release.Name }}-serving-cert +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ .Release.Name }}-selfsigned-issuer + namespace: {{ .Release.Namespace }} +spec: + selfSigned: {} +{{- end }} \ No newline at end of file diff --git a/deploy/operator/templates/hooks/crd-installer-job.yaml b/deploy/operator/templates/hooks/crd-installer-job.yaml new file mode 100644 index 00000000..ae859f79 --- /dev/null +++ b/deploy/operator/templates/hooks/crd-installer-job.yaml @@ -0,0 +1,89 @@ +{{- /* + Pre-install / pre-upgrade Helm hook Job that installs CRDs for the operator + itself and any enabled subchart whose CRDs we manage (redis, clickhouse, etc.). + + The Job runs the operator image's /crd-installer binary, which embeds the + CRD YAML at build time and applies it via server-side apply with field + manager "wandb-crd-installer". cert-manager's CA injector owns the + caBundle field separately, so SSA preserves it across re-applies. + + Hook lifecycle: + - pre-install, pre-upgrade — runs on both fresh installs and upgrades. + - weight 0 (RBAC at weight -10 in rbac.yaml runs first). + - before-hook-creation,hook-succeeded — re-runs are idempotent; cleanup + on success keeps the cluster tidy. +*/ -}} +{{- if .Values.helmHooks.enabled }} +{{- $image := printf "%s:%s" (index .Values "wandb-operator" "image" "repository") (index .Values "wandb-operator" "image" "tag") -}} +{{- /* + Honor the same imagePullSecrets the main operator pod uses. wandb-base + concatenates the subchart's own list with .Values.global.imagePullSecrets, + so mirror both sources here. +*/ -}} +{{- $operatorPullSecrets := default list (index .Values "wandb-operator" "imagePullSecrets") -}} +{{- $globalPullSecrets := default list (dig "global" "imagePullSecrets" (list) .Values.AsMap) -}} +{{- $combinedPullSecrets := concat $operatorPullSecrets $globalPullSecrets -}} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "wandb-operator.crdInstallerName" . }} + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook": "pre-install,pre-upgrade" + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" +spec: + backoffLimit: 3 + ttlSecondsAfterFinished: 300 + template: + metadata: + labels: + app.kubernetes.io/component: crd-installer + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + restartPolicy: OnFailure + serviceAccountName: {{ include "wandb-operator.crdInstallerName" . }} + {{- if $combinedPullSecrets }} + imagePullSecrets: + {{- range $secret := $combinedPullSecrets }} + {{- if kindIs "string" $secret }} + - name: {{ tpl $secret $ }} + {{- else if and (kindIs "map" $secret) (hasKey $secret "name") }} + - name: {{ tpl $secret.name $ }} + {{- end }} + {{- end }} + {{- end }} + securityContext: + runAsNonRoot: {{ dig "wandb-operator" "podSecurityContext" "runAsNonRoot" true .Values.AsMap }} + {{- /* runAsUser only if set, else restricted-v2 assigns it. */ -}} + {{- $crdInstallerRunAsUser := dig "wandb-operator" "podSecurityContext" "runAsUser" nil .Values.AsMap }} + {{- if $crdInstallerRunAsUser }} + runAsUser: {{ $crdInstallerRunAsUser }} + {{- end }} + seccompProfile: + type: {{ dig "wandb-operator" "podSecurityContext" "seccompProfile" "type" "RuntimeDefault" .Values.AsMap }} + containers: + - name: crd-installer + image: {{ $image | quote }} + imagePullPolicy: {{ index .Values "wandb-operator" "image" "pullPolicy" | default "IfNotPresent" }} + command: ["/crd-installer"] + args: + - apply + - --cert-inject-reference={{ .Release.Namespace }}/{{ .Release.Name }}-serving-cert + - --webhook-service-name={{ include "wandb-operator.webhookServiceName" . }} + - --webhook-service-namespace={{ .Release.Namespace }} + - --groups={{ include "wandb-operator.crdGroups" . }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi +{{- end }} diff --git a/deploy/operator/templates/hooks/rbac.yaml b/deploy/operator/templates/hooks/rbac.yaml new file mode 100644 index 00000000..3f414201 --- /dev/null +++ b/deploy/operator/templates/hooks/rbac.yaml @@ -0,0 +1,48 @@ +{{- if .Values.helmHooks.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "wandb-operator.crdInstallerName" . }} + namespace: {{ .Release.Namespace }} + annotations: + "helm.sh/hook": "pre-install,pre-upgrade" + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "wandb-operator.crdInstallerName" . }} + annotations: + "helm.sh/hook": "pre-install,pre-upgrade" + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" +rules: + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - list + - create + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "wandb-operator.crdInstallerName" . }} + annotations: + "helm.sh/hook": "pre-install,pre-upgrade" + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "wandb-operator.crdInstallerName" . }} +subjects: + - kind: ServiceAccount + name: {{ include "wandb-operator.crdInstallerName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/operator/templates/openshift-owner-finalizers-rbac.yaml b/deploy/operator/templates/openshift-owner-finalizers-rbac.yaml new file mode 100644 index 00000000..3157bd7e --- /dev/null +++ b/deploy/operator/templates/openshift-owner-finalizers-rbac.yaml @@ -0,0 +1,56 @@ +{{- /* OpenShift-only: finalizers grants for moco + seaweedfs owners. */ -}} +{{- if dig "openshift" "enabled" false .Values.AsMap }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Release.Name }}-openshift-owner-finalizers + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: wandb-operator +rules: + - apiGroups: + - moco.cybozu.com + resources: + - mysqlclusters/finalizers + verbs: + - update + - apiGroups: + - seaweed.seaweedfs.com + resources: + - seaweeds/finalizers + verbs: + - update +--- +# StatefulSet controller creates moco PVCs, so it sets mysqlcluster finalizers. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Name }}-openshift-mysqlcluster-finalizers + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: wandb-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Release.Name }}-openshift-owner-finalizers +subjects: + - kind: ServiceAccount + name: statefulset-controller + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Name }}-openshift-seaweed-finalizers + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: wandb-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Release.Name }}-openshift-owner-finalizers +subjects: + - kind: ServiceAccount + name: {{ .Release.Name }}-seaweedfs-operator + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/deploy/operator/templates/openshift-scc-rbac.yaml b/deploy/operator/templates/openshift-scc-rbac.yaml new file mode 100644 index 00000000..3a9c118f --- /dev/null +++ b/deploy/operator/templates/openshift-scc-rbac.yaml @@ -0,0 +1,35 @@ +{{- /* OpenShift-only: operator needs `use` on the SCCs it grants. */ -}} +{{- if dig "openshift" "enabled" false .Values.AsMap }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Release.Name }}-openshift-scc-nonroot-v2 + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: wandb-operator +rules: + - apiGroups: + - security.openshift.io + resources: + - securitycontextconstraints + resourceNames: + - nonroot-v2 + verbs: + - use +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Name }}-openshift-scc-nonroot-v2 + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/part-of: wandb-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Release.Name }}-openshift-scc-nonroot-v2 +subjects: + - kind: ServiceAccount + name: {{ include "wandb-operator.fullname" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/deploy/operator/templates/seaweedfs-operator-servicemonitor-role.yaml b/deploy/operator/templates/seaweedfs-operator-servicemonitor-role.yaml new file mode 100644 index 00000000..a6e1b788 --- /dev/null +++ b/deploy/operator/templates/seaweedfs-operator-servicemonitor-role.yaml @@ -0,0 +1,32 @@ +{{- if (index .Values "seaweedfs-operator" "install") }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Release.Name }}-seaweedfs-operator-servicemonitor +rules: + - apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Name }}-seaweedfs-operator-servicemonitor +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Release.Name }}-seaweedfs-operator-servicemonitor +subjects: + - kind: ServiceAccount + name: {{ .Release.Name }}-seaweedfs-operator + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/deploy/operator/templates/telemetry-config.yaml b/deploy/operator/templates/telemetry-config.yaml new file mode 100644 index 00000000..34027bb1 --- /dev/null +++ b/deploy/operator/templates/telemetry-config.yaml @@ -0,0 +1,20 @@ +{{- $operatorEnabled := true -}} +{{- $operatorValues := index .Values "wandb-operator" -}} +{{- if and $operatorValues (hasKey $operatorValues "enabled") -}} +{{- $operatorEnabled = index $operatorValues "enabled" -}} +{{- end -}} +{{- if and $operatorEnabled (ne (default "off" .Values.telemetry.mode) "off") }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: wandb-operator-telemetry-config + namespace: {{ .Release.Namespace }} +data: + TELEMETRY_ENABLED: {{ ne (default "off" .Values.telemetry.mode) "off" | quote }} + TELEMETRY_MODE: {{ default "off" .Values.telemetry.mode | quote }} + TELEMETRY_MANAGED_NAMESPACE: {{ .Values.telemetry.namespace | quote }} + TELEMETRY_OTEL_SECRET_NAME: {{ .Values.telemetry.otel.secretName | quote }} + TELEMETRY_OTEL_PROTOCOL: {{ .Values.telemetry.otel.protocol | quote }} + TELEMETRY_OTEL_SERVICE_NAME: {{ .Values.telemetry.otel.serviceName | quote }} + TELEMETRY_OTEL_RESOURCE_ATTRIBUTES: {{ .Values.telemetry.otel.resourceAttributes | quote }} +{{- end }} diff --git a/deploy/operator/templates/telemetry-validation.yaml b/deploy/operator/templates/telemetry-validation.yaml new file mode 100644 index 00000000..a93e0c77 --- /dev/null +++ b/deploy/operator/templates/telemetry-validation.yaml @@ -0,0 +1,26 @@ +{{- $mode := default "off" .Values.telemetry.mode -}} +{{- $stackEnabled := or (eq $mode "forward") (eq $mode "full") -}} +{{- $uiEnabled := eq $mode "full" -}} +{{- $operatorEnabled := true -}} +{{- $operatorValues := index .Values "wandb-operator" -}} +{{- if and $operatorValues (hasKey $operatorValues "enabled") -}} +{{- $operatorEnabled = index $operatorValues "enabled" -}} +{{- end -}} +{{- if and $operatorEnabled (eq $mode "off") (index .Values "victoria-metrics-operator" "enabled") -}} +{{- fail "telemetry.mode=off requires victoria-metrics-operator.enabled=false" -}} +{{- end -}} +{{- if and $operatorEnabled (eq $mode "off") (index .Values "grafana-operator" "enabled") -}} +{{- fail "telemetry.mode=off requires grafana-operator.enabled=false" -}} +{{- end -}} +{{- if and $operatorEnabled $stackEnabled (not (index .Values "victoria-metrics-operator" "enabled")) -}} +{{- fail "telemetry.mode=forward/full requires victoria-metrics-operator.enabled=true" -}} +{{- end -}} +{{- if and $operatorEnabled $uiEnabled (not (index .Values "grafana-operator" "enabled")) -}} +{{- fail "telemetry.mode=full requires grafana-operator.enabled=true" -}} +{{- end -}} +{{- if and $operatorEnabled $stackEnabled (eq (default "" .Values.telemetry.otel.secretName) "") -}} +{{- fail "telemetry.mode=forward/full requires telemetry.otel.secretName" -}} +{{- end -}} +{{- if and (eq $mode "forward") (eq (default "" .Values.telemetry.forwarding.otlp.endpoint) "") -}} +{{- fail "telemetry.mode=forward requires telemetry.forwarding.otlp.endpoint" -}} +{{- end -}} diff --git a/deploy/operator/templates/wandb-cr.yaml b/deploy/operator/templates/wandb-cr.yaml new file mode 100644 index 00000000..fa2947e2 --- /dev/null +++ b/deploy/operator/templates/wandb-cr.yaml @@ -0,0 +1,44 @@ +{{ if and .Values.wandb.install (not (eq (index .Values "wandb-operator" "enabled") false)) }} +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .Values.wandb.namespace }} + +--- +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb + namespace: {{ .Values.wandb.namespace }} +spec: + wandb: + internalServiceAuth: + enabled: {{ .Values.wandb.internalServiceAuth }} + hostname: http://localhost + features: + proxy: true + version: {{ .Values.wandb.version }} + size: {{ .Values.wandb.size }} + retentionPolicy: + onDelete: detach + mysql: + managedMysql: + telemetry: + enabled: false + redis: + managedRedis: + telemetry: + enabled: false + kafka: + managedKafka: + telemetry: + enabled: false + objectStore: + managedObjectStore: + telemetry: + enabled: false + clickhouse: + managedClickhouse: + telemetry: + enabled: false +{{ end }} diff --git a/deploy/operator/templates/wandb-operator-application-role.yaml b/deploy/operator/templates/wandb-operator-application-role.yaml new file mode 100644 index 00000000..144c751c --- /dev/null +++ b/deploy/operator/templates/wandb-operator-application-role.yaml @@ -0,0 +1,71 @@ +{{- if (dig "wandb-operator" "enabled" true .Values.AsMap) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Release.Name }}-application +rules: + - apiGroups: + - apps + resources: + - controllerrevisions + - daemonsets + - deployments + - replicasets + - statefulsets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apps + resources: + - daemonsets/status + - deployments/status + - replicasets/status + - statefulsets/status + verbs: + - get + - apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - batch + resources: + - cronjobs + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Name }}-application +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Release.Name }}-application +subjects: + - kind: ServiceAccount + name: {{ include "wandb-operator.fullname" . }} + namespace: {{ .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/operator/templates/wandb-operator-clickhouse-role.yaml b/deploy/operator/templates/wandb-operator-clickhouse-role.yaml new file mode 100644 index 00000000..23cb949c --- /dev/null +++ b/deploy/operator/templates/wandb-operator-clickhouse-role.yaml @@ -0,0 +1,57 @@ +{{- if (dig "wandb-operator" "enabled" true .Values.AsMap) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Release.Name }}-clickhouse +rules: + - apiGroups: + - clickhouse.altinity.com + resources: + - clickhouseinstallations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - clickhouse.altinity.com + resources: + - clickhouseinstallations/status + verbs: + - get + - apiGroups: + - clickhouse-keeper.altinity.com + resources: + - clickhousekeeperinstallations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - clickhouse-keeper.altinity.com + resources: + - clickhousekeeperinstallations/status + verbs: + - get + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Name }}-clickhouse +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Release.Name }}-clickhouse +subjects: + - kind: ServiceAccount + name: {{ include "wandb-operator.fullname" . }} + namespace: {{ .Release.Namespace }} + {{- end }} \ No newline at end of file diff --git a/deploy/operator/templates/wandb-operator-grafana-role.yaml b/deploy/operator/templates/wandb-operator-grafana-role.yaml new file mode 100644 index 00000000..df61096f --- /dev/null +++ b/deploy/operator/templates/wandb-operator-grafana-role.yaml @@ -0,0 +1,45 @@ +{{- if and (eq (default "off" .Values.telemetry.mode) "full") (dig "wandb-operator" "enabled" true .Values.AsMap) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Release.Name }}-grafana +rules: + - apiGroups: + - grafana.integreatly.org + resources: + - grafanas + - grafanadashboards + - grafanadatasources + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - grafana.integreatly.org + resources: + - grafanas/status + - grafanadashboards/status + - grafanadatasources/status + verbs: + - get + - patch + - update + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Name }}-grafana +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Release.Name }}-grafana +subjects: + - kind: ServiceAccount + name: {{ include "wandb-operator.fullname" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/deploy/operator/templates/wandb-operator-mysql-role.yaml b/deploy/operator/templates/wandb-operator-mysql-role.yaml new file mode 100644 index 00000000..d9a84422 --- /dev/null +++ b/deploy/operator/templates/wandb-operator-mysql-role.yaml @@ -0,0 +1,41 @@ +{{- if (dig "wandb-operator" "enabled" true .Values.AsMap) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Release.Name }}-mysql +rules: + - apiGroups: + - moco.cybozu.com + resources: + - mysqlclusters + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - moco.cybozu.com + resources: + - mysqlclusters/status + verbs: + - get + - patch + - update + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Name }}-mysql +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Release.Name }}-mysql +subjects: + - kind: ServiceAccount + name: {{ include "wandb-operator.fullname" . }} + namespace: {{ .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/operator/templates/wandb-operator-redis-role.yaml b/deploy/operator/templates/wandb-operator-redis-role.yaml new file mode 100644 index 00000000..a164263b --- /dev/null +++ b/deploy/operator/templates/wandb-operator-redis-role.yaml @@ -0,0 +1,41 @@ +{{- if (dig "wandb-operator" "enabled" true .Values.AsMap) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Release.Name }}-redis +rules: + - apiGroups: + - redis.redis.opstreelabs.in + resources: + - redis + - redisreplications + - redissentinels + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - redis.redis.opstreelabs.in + resources: + - redis/status + verbs: + - get + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Name }}-redis +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Release.Name }}-redis +subjects: + - kind: ServiceAccount + name: {{ include "wandb-operator.fullname" . }} + namespace: {{ .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/operator/templates/wandb-operator-seaweedfs-role.yaml b/deploy/operator/templates/wandb-operator-seaweedfs-role.yaml new file mode 100644 index 00000000..18ee5a47 --- /dev/null +++ b/deploy/operator/templates/wandb-operator-seaweedfs-role.yaml @@ -0,0 +1,40 @@ +{{- if (dig "wandb-operator" "enabled" true .Values.AsMap) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Release.Name }}-seaweedfs +rules: + - apiGroups: + - seaweed.seaweedfs.com + resources: + - seaweeds + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - seaweed.seaweedfs.com + resources: + - seaweeds/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Name }}-seaweedfs +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Release.Name }}-seaweedfs +subjects: + - kind: ServiceAccount + name: {{ include "wandb-operator.fullname" . }} + namespace: {{ .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/operator/templates/wandb-operator-vm-role.yaml b/deploy/operator/templates/wandb-operator-vm-role.yaml new file mode 100644 index 00000000..6ca2fef5 --- /dev/null +++ b/deploy/operator/templates/wandb-operator-vm-role.yaml @@ -0,0 +1,53 @@ +{{- if and (ne (default "off" .Values.telemetry.mode) "off") (dig "wandb-operator" "enabled" true .Values.AsMap) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Release.Name }}-vm +rules: + - apiGroups: + - operator.victoriametrics.com + resources: + - vmagents + - vmalerts + - vmnodescrapes + - vmpodscrapes + - vmrules + - vmservicescrapes + - vmsingles + - vlsingles + - vtsingles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - operator.victoriametrics.com + resources: + - vmagents/status + - vmalerts/status + - vmsingles/status + - vlsingles/status + - vtsingles/status + verbs: + - get + - patch + - update + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Name }}-vm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Release.Name }}-vm +subjects: + - kind: ServiceAccount + name: {{ include "wandb-operator.fullname" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/deploy/operator/templates/wandb-operator-wandb-role.yaml b/deploy/operator/templates/wandb-operator-wandb-role.yaml new file mode 100644 index 00000000..290ae4e8 --- /dev/null +++ b/deploy/operator/templates/wandb-operator-wandb-role.yaml @@ -0,0 +1,213 @@ +{{- if (dig "wandb-operator" "enabled" true .Values.AsMap) }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Release.Name }}-wandb +rules: + - apiGroups: + - apps.wandb.com + resources: + - applications + - weightsandbiases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - apps.wandb.com + resources: + - applications/finalizers + - weightsandbiases/finalizers + verbs: + - update + - apiGroups: + - apps.wandb.com + resources: + - applications/status + - weightsandbiases/status + verbs: + - get + - patch + - update + - apiGroups: + - "" + resources: + - configmaps + - events + - persistentvolumeclaims + - secrets + - serviceaccounts + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - pods + - pods/log + - pods/status + - namespaces + - namespaces/status + verbs: + - get + - list + - watch + - patch + - apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses + - networkpolicies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - networking.k8s.io + resources: + - ingresses/status + verbs: + - get + - list + - watch + - apiGroups: + - gateway.networking.k8s.io + resources: + - backendtlspolicies + - gateways + - httproutes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - gateway.networking.k8s.io + resources: + - backendtlspolicies/status + - gateways/status + - httproutes/status + verbs: + - get + - apiGroups: + - gateway.nginx.org + resources: + - clientsettingspolicies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - networking.gke.io + resources: + - healthcheckpolicies + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Release.Name }}-wandb +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Release.Name }}-wandb +subjects: + - kind: ServiceAccount + name: {{ include "wandb-operator.fullname" . }} + namespace: {{ .Release.Namespace }} + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Release.Name }}-leader-election +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Release.Name }}-leader-election +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Release.Name }}-leader-election +subjects: + - kind: ServiceAccount + name: {{ include "wandb-operator.fullname" . }} + namespace: {{ .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/deploy/operator/templates/webhooks.yaml b/deploy/operator/templates/webhooks.yaml new file mode 100644 index 00000000..a17d1254 --- /dev/null +++ b/deploy/operator/templates/webhooks.yaml @@ -0,0 +1,98 @@ +{{- if (dig "wandb-operator" "enabled" true .Values.AsMap) }} +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ .Release.Name }}-serving-cert + name: {{ .Release.Name }}-mutating-webhook-configuration +webhooks: + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "wandb-operator.serviceName" . }} + namespace: {{ .Release.Namespace }} + path: /mutate-apps-wandb-com-v2-application + failurePolicy: Fail + name: mapplication-v2.kb.io + rules: + - apiGroups: + - apps.wandb.com + apiVersions: + - v2 + operations: + - CREATE + - UPDATE + resources: + - applications + sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "wandb-operator.serviceName" . }} + namespace: {{ .Release.Namespace }} + path: /mutate-apps-wandb-com-v2-weightsandbiases + failurePolicy: Fail + name: mweightsandbiases-v2.kb.io + rules: + - apiGroups: + - apps.wandb.com + apiVersions: + - v2 + operations: + - CREATE + - UPDATE + resources: + - weightsandbiases + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ .Release.Name }}-serving-cert + name: {{ .Release.Name }}-validating-webhook-configuration +webhooks: + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "wandb-operator.serviceName" . }} + namespace: {{ .Release.Namespace }} + path: /validate-apps-wandb-com-v2-application + failurePolicy: Fail + name: vapplication-v2.kb.io + rules: + - apiGroups: + - apps.wandb.com + apiVersions: + - v2 + operations: + - CREATE + - UPDATE + resources: + - applications + sideEffects: None + - admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "wandb-operator.serviceName" . }} + namespace: {{ .Release.Namespace }} + path: /validate-apps-wandb-com-v2-weightsandbiases + failurePolicy: Fail + name: vweightsandbiases-v2.kb.io + rules: + - apiGroups: + - apps.wandb.com + apiVersions: + - v2 + operations: + - CREATE + - UPDATE + resources: + - weightsandbiases + sideEffects: None +{{- end }} \ No newline at end of file diff --git a/deploy/operator/values.schema.json b/deploy/operator/values.schema.json new file mode 100644 index 00000000..42630cc6 --- /dev/null +++ b/deploy/operator/values.schema.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "telemetry": { + "type": "object", + "additionalProperties": false, + "properties": { + "mode": { + "type": "string", + "enum": ["off", "forward", "full"] + }, + "namespace": { + "type": "string" + }, + "otel": { + "type": "object", + "additionalProperties": false, + "properties": { + "secretName": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "serviceName": { + "type": "string" + }, + "resourceAttributes": { + "type": "string" + } + } + }, + "forwarding": { + "type": "object", + "additionalProperties": false, + "properties": { + "otlp": { + "type": "object", + "additionalProperties": false, + "properties": { + "endpoint": { + "type": "string" + }, + "protocol": { + "type": "string", + "enum": ["http/protobuf", "grpc"] + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "tls": { + "type": "object", + "additionalProperties": false, + "properties": { + "insecure": { + "type": "boolean" + } + } + } + } + } + } + }, + "retentionPeriod": { + "type": "string", + "minLength": 1 + }, + "otlpGateway": { + "type": "object", + "additionalProperties": false, + "properties": { + "podSecurityContext": { + "type": "object", + "additionalProperties": true + }, + "securityContext": { + "type": "object", + "additionalProperties": true + } + } + }, + "scrape": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "kubeletCadvisor": { + "type": "boolean" + }, + "operators": { + "type": "boolean" + }, + "wandbApi": { + "type": "boolean" + }, + "infrastructure": { + "type": "boolean" + }, + "kubeStateMetrics": { + "type": "boolean" + } + } + }, + "alerting": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "evaluationInterval": { + "type": "string", + "minLength": 1 + }, + "notifier": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "type": { + "type": "string", + "enum": ["webhook", "email", "slack"] + }, + "target": { + "type": "string", + "minLength": 1 + } + } + } + } + }, + "global": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "additionalProperties": true +} diff --git a/deploy/operator/values.yaml b/deploy/operator/values.yaml new file mode 100644 index 00000000..fc546967 --- /dev/null +++ b/deploy/operator/values.yaml @@ -0,0 +1,258 @@ +helmHooks: + enabled: true + +# Renders OpenShift-only resources; profiles/openshift.yaml turns this on. +openshift: + enabled: false + +wandb: + install: true + size: small + namespace: wandb + version: 0.81.0 + internalServiceAuth: false + +wandb-operator: + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/operator + tag: 2.0.0-alpha.2 + containers: + operator: + command: + - /manager + args: + - --metrics-bind-address=:8443 + - --leader-elect + - --health-probe-bind-address=:8081 + - --webhook-cert-path=/tmp/wandb-operator/serving-certs + ports: + - containerPort: 8443 + name: metrics + protocol: TCP + env: + METRICS_BIND_ADDRESS: + value: ":8443" + METRICS_SECURE: + value: "false" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - "ALL" + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 10m + memory: 64Mi + volumeMounts: + - mountPath: /tmp/server-manifest + name: server-manifest + - mountPath: /tmp/wandb-operator/serving-certs + name: serving-certs + readOnly: true + + service: + enabled: true + ports: + - name: webhook + port: 443 + protocol: TCP + targetPort: 9443 + - name: metrics + port: 8443 + protocol: TCP + targetPort: 8443 + replicaCount: 1 + podSecurityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + fsGroupChangePolicy: OnRootMismatch + seccompProfile: + type: RuntimeDefault + volumes: + - name: server-manifest + emptyDir: {} + - name: serving-certs + secret: + secretName: '{{ .Release.Name }}-serving-cert' + + operators: + application: true + clickhouse: true + grafana: true + kafka: true + objectStore: true + mysql: true + redis: true + vm: true + +moco: + enabled: true + +redis-operator: + enabled: true + podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - "ALL" + +seaweedfs-operator: + enabled: true + podSecurityContext: + seccompProfile: + type: RuntimeDefault + webhook: + enabled: false + +# Only the ServiceMonitor CRD is needed — the seaweedfs-operator creates them +# during reconcile and the VictoriaMetrics Operator auto-converts them to +# VMServiceScrapes. +prometheus-operator-crds: + crds: + alertmanagerconfigs: + enabled: false + alertmanagers: + enabled: false + podmonitors: + enabled: false + probes: + enabled: false + prometheusagents: + enabled: false + prometheuses: + enabled: false + prometheusrules: + enabled: false + scrapeconfigs: + enabled: false + servicemonitors: + enabled: true + thanosrulers: + enabled: false + +altinity-clickhouse-operator: + crdHook: + enabled: false + enabled: true + podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + seccompProfile: + type: RuntimeDefault + operator: + containerSecurityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - "ALL" + metrics: + containerSecurityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - "ALL" + configs: + files: + config.yaml: + watch: + namespaces: [".*"] + +victoria-metrics-operator: + # Helm dependency conditions are boolean-only, so full/forward installs + # must opt into this dependency explicitly or via a preset values file. + enabled: false + podSecurityContext: + seccompProfile: + type: RuntimeDefault + crds: + # Install Victoria CRDs through Helm's CRD phase so telemetry resources + # in this chart can be created in the same release. + plain: true + +grafana-operator: + # Helm dependency conditions are boolean-only, so full installs + # must opt into this dependency explicitly or via a preset values file. + enabled: false + podSecurityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + +telemetry: + # off: disable telemetry collection + # full: run the in-cluster Victoria stack and local Grafana + # forward: run the in-cluster Victoria stack and forward OTLP data externally + mode: "off" + namespace: wandb + otel: + secretName: wandb-otel-connection + protocol: http/protobuf + serviceName: wandb-service + resourceAttributes: "" + forwarding: + otlp: + endpoint: "" + protocol: http/protobuf + headers: {} + tls: + insecure: false + retentionPeriod: 1d + otlpGateway: + podSecurityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + runAsNonRoot: true + scrape: + enabled: true + kubeletCadvisor: true + operators: true + wandbApi: true + infrastructure: true + kubeStateMetrics: false + alerting: + enabled: false + evaluationInterval: 30s + notifier: + enabled: false + name: default + type: webhook + target: http://alertmanager-operated.monitoring.svc:9093 diff --git a/deploy/telemetry/Chart.yaml b/deploy/telemetry/Chart.yaml new file mode 100644 index 00000000..f32124e7 --- /dev/null +++ b/deploy/telemetry/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: telemetry +description: A Helm chart for the managed W&B telemetry stack +type: application +version: 0.1.0 +appVersion: "1.0.0" +maintainers: + - name: wandb + email: support@wandb.com + url: https://wandb.com diff --git a/deploy/telemetry/dashboards/wandb-application.json b/deploy/telemetry/dashboards/wandb-application.json new file mode 100644 index 00000000..f32f803a --- /dev/null +++ b/deploy/telemetry/dashboards/wandb-application.json @@ -0,0 +1,558 @@ +{ + "__inputs": [ + { + "name": "DS_VICTORIAMETRICS", + "label": "VictoriaMetrics", + "type": "datasource", + "pluginId": "victoriametrics-metrics-datasource", + "pluginName": "VictoriaMetrics" + }, + {"name": "DS_VICTORIATRACES", "label": "VictoriaTraces", "type": "datasource", "pluginId": "jaeger", "pluginName": "Jaeger"} + ], + "annotations": {"list": []}, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": true, + "targetBlank": true, + "title": "Open Traces in Explore", + "type": "link", + "url": "/explore?panes=%7B%22A%22:%7B%22datasource%22:%22${DS_VICTORIATRACES}%22,%22queries%22:[],%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&schemaVersion=1&orgId=1" + }, + { + "asDropdown": false, + "icon": "dashboard", + "includeVars": true, + "keepTime": true, + "targetBlank": false, + "title": "W&B Field Investigation", + "type": "link", + "url": "/d/wandb-field-investigation" + } + ], + "panels": [ + { + "type": "text", + "title": "", + "gridPos": {"h": 6, "w": 24, "x": 0, "y": 0}, + "options": { + "mode": "markdown", + "content": "# W&B Application\n\nAnswers **\"is the whole W&B app slow or erroring for this customer?\"** Start here when a customer reports the UI is slow, requests fail, or the SDK can't reach the server.\n\nThe top sections cover **overall API health** — request rate, error codes, and latency. The **GraphQL errors** section attributes failures to the specific service/operation responsible. The **Operations & background** section shows read-path, artifact, and export span latencies from traces.\n\nUse the **W&B Namespace** dropdown to pick the customer's install. Use **Open Traces in Explore** (top-right) to drill from any symptom to a real trace." + } + }, + { + "type": "row", + "title": "Request health", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 6}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "Request rate by status code", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 8, "w": 16, "x": 0, "y": 7}, + "targets": [ + { + "expr": "sum by (http_response_status_code) (rate(http_server_request_duration_seconds_count{service_name=\"gorilla\"}[$__rate_interval]))", + "legendFormat": "{{http_response_status_code}}", + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "reqps", "min": 0}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi"}} + }, + { + "type": "text", + "title": "About: Request rate by status code", + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 7}, + "options": { + "mode": "markdown", + "content": "**What it shows** — every HTTP request the W&B API serves, per second, split by response code. This is the single best \"is the app up?\" view.\n\n**What bad looks like** — a wall of `5xx` appearing, or `2xx` dropping to near-zero (traffic no longer reaching the API: ingress / service / DNS / pod crash).\n\n**Do next** — 5xx climbing → check pod logs. To attribute GraphQL errors to a feature, see the **GraphQL errors** section below." + } + }, + { + "type": "timeseries", + "title": "4xx vs 5xx request rate", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 8, "w": 16, "x": 0, "y": 15}, + "targets": [ + { + "expr": "sum(rate(http_server_request_duration_seconds_count{service_name=\"gorilla\", http_response_status_code=~\"4..\"}[$__rate_interval])) or vector(0)", + "legendFormat": "4xx (client)", + "refId": "A" + }, + { + "expr": "sum(rate(http_server_request_duration_seconds_count{service_name=\"gorilla\", http_response_status_code=~\"5..\"}[$__rate_interval])) or vector(0)", + "legendFormat": "5xx (server)", + "refId": "B" + } + ], + "fieldConfig": {"defaults": {"unit": "reqps", "min": 0}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi"}} + }, + { + "type": "text", + "title": "About: 4xx vs 5xx", + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 15}, + "options": { + "mode": "markdown", + "content": "**What it shows** — client errors (`4xx`) vs server errors (`5xx`) as two clean lines, so you can tell whose fault it is.\n\n**What bad looks like** — any sustained `5xx` is a server problem. A `4xx` spike is usually the customer's SDK/auth (version mismatch, bad token, malformed query) and is often *not* an outage.\n\n**Do next** — 5xx → check API pod logs. 4xx → confirm SDK version against **Service Image Versions** on [W&B Field Investigation](/d/wandb-field-investigation).\n\n**Note** — GraphQL errors return HTTP `200` (the error is in the response body), so they don't appear here; use the **GraphQL errors** section below for those. This panel catches HTTP transport/server failures; a flat `0` means healthy." + } + }, + { + "type": "timeseries", + "title": "Error ratio (5xx / all requests)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 8, "w": 16, "x": 0, "y": 23}, + "targets": [ + { + "expr": "(sum(rate(http_server_request_duration_seconds_count{service_name=\"gorilla\", http_response_status_code=~\"5..\"}[$__rate_interval])) or vector(0)) / sum(rate(http_server_request_duration_seconds_count{service_name=\"gorilla\"}[$__rate_interval]))", + "legendFormat": "5xx ratio", + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "percentunit", "min": 0}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi"}} + }, + { + "type": "text", + "title": "About: Error ratio", + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 23}, + "options": { + "mode": "markdown", + "content": "**What it shows** — the share of requests failing with a server error, as a percentage. Normalizes for traffic so a busy install and a quiet one read the same.\n\n**What bad looks like** — above ~1% sustained is a real incident; a brief blip during a deploy is usually fine.\n\n**Do next** — if the ratio is high, capture a snapshot of this panel and use the **GraphQL errors** section below to see which feature is responsible, then drill to a trace.\n\n**Note** — this is HTTP `5xx` only; GraphQL errors are HTTP `200`, so a `0` ratio doesn't mean error-free — check the **GraphQL errors** section below." + } + }, + { + "type": "row", + "title": "Latency", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 31}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "Request latency (p50 / p95 / p99)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 8, "w": 16, "x": 0, "y": 32}, + "targets": [ + { + "expr": "histogram_quantile(0.50, sum by(le)(rate(http_server_request_duration_seconds_bucket{service_name=\"gorilla\"}[$__rate_interval])))", + "legendFormat": "p50", + "refId": "A" + }, + { + "expr": "histogram_quantile(0.95, sum by(le)(rate(http_server_request_duration_seconds_bucket{service_name=\"gorilla\"}[$__rate_interval])))", + "legendFormat": "p95", + "refId": "B" + }, + { + "expr": "histogram_quantile(0.99, sum by(le)(rate(http_server_request_duration_seconds_bucket{service_name=\"gorilla\"}[$__rate_interval])))", + "legendFormat": "p99", + "refId": "C" + } + ], + "fieldConfig": {"defaults": {"unit": "s", "min": 0}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi"}}, + "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "yellow", "value": 1}, {"color": "red", "value": 3}]} + }, + { + "type": "text", + "title": "About: Request latency", + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 32}, + "options": { + "mode": "markdown", + "content": "**What it shows** — how long requests take, across all gorilla requests. p50 is the typical user; p95/p99 are the slowest 5% / 1%.\n\n**What bad looks like** — p95 above ~3s sustained for more than a few minutes means users feel it. Brief spikes under load are tolerable.\n\n**Do next** — if p95/p99 are high, use **p95 by operation** and **Top operations by slow request count** below to find the culprit, then Open Traces in Explore (top-right) and filter `duration>3s`." + } + }, + { + "type": "timeseries", + "title": "p95 latency by operation (top 10)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 8, "w": 16, "x": 0, "y": 40}, + "targets": [ + { + "expr": "topk(10, histogram_quantile(0.95, sum by(le, span_name)(rate(traces_spanmetrics_duration_milliseconds_bucket{service_name=\"gorilla\"}[$__rate_interval]))))", + "legendFormat": "{{span_name}}", + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "ms", "min": 0}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "right", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}} + }, + { + "type": "text", + "title": "About: p95 by operation", + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 40}, + "options": { + "mode": "markdown", + "content": "**What it shows** — the 10 operations (trace span names) with the highest p95 latency, over time. Tells you *which operation* is slow, not just that something is. Sourced from spanmetrics (`traces_spanmetrics_*`, milliseconds) since gorilla's HTTP metrics don't carry a route label.\n\n**What bad looks like** — one operation sitting well above the rest, especially graphql spans climbing after a deploy.\n\n**Do next** — a slow graphql span → see the **GraphQL errors** section below; a slow read/export span → see **Operations & background** below. Then drill to a trace for that operation." + } + }, + { + "type": "table", + "title": "Top operations by slow request count (>2.5s)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 48}, + "targets": [ + { + "expr": "topk(20, sum by (span_name) (increase(traces_spanmetrics_duration_milliseconds_count{service_name=\"gorilla\"}[$__range]) - increase(traces_spanmetrics_duration_milliseconds_bucket{service_name=\"gorilla\", le=\"2500\"}[$__range])))", + "format": "table", + "instant": true, + "refId": "A" + } + ], + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": {"Time": true}, + "indexByName": {"span_name": 0, "Value": 1}, + "renameByName": {"span_name": "Operation", "Value": "Slow requests (>2.5s) in range"} + } + } + ], + "options": { + "showHeader": true, + "cellHeight": "md", + "footer": {"show": true, "reducer": ["sum"], "fields": ["Value"]}, + "sortBy": [{"displayName": "Slow requests (>2.5s) in range", "desc": true}] + }, + "fieldConfig": { + "defaults": {"custom": {"align": "left", "filterable": true}}, + "overrides": [ + { + "matcher": {"id": "byName", "options": "Slow requests (>2.5s) in range"}, + "properties": [ + {"id": "custom.cellOptions", "value": {"type": "gauge", "mode": "gradient"}}, + {"id": "custom.align", "value": "right"} + ] + } + ] + } + }, + { + "type": "text", + "title": "About: Top operations by slow request count", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 48}, + "options": { + "mode": "markdown", + "content": "**What it shows** — the operations that produced the most requests slower than 2.5 seconds over the selected range (counted from the latency histogram). This is the volume view of slowness: which operations hurt the most users.\n\n**What bad looks like** — an operation with a large, growing count of >2.5s requests, especially the workspace, run-table, or artifact operations.\n\n**Do next** — note the top operation, Open Traces in Explore (top-right), filter `service.name=gorilla` and `duration>2.5s` on that operation to read a real slow trace." + } + }, + { + "type": "row", + "title": "GraphQL operations", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 58}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "GraphQL operation rate by span (top, sampled)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 8, "w": 16, "x": 0, "y": 59}, + "targets": [ + { + "expr": "topk(10, sum by (span_name) (rate(traces_spanmetrics_calls_milliseconds_total{graphql_service!=\"\"}[$__rate_interval])))", + "legendFormat": "{{span_name}}", + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "reqps", "min": 0}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "right", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, + "description": "From spanmetrics (traces), which are sampled — trust the shape, not the absolute count. Spanmetrics have NO namespace label, so this is not filtered by $namespace; it reflects the whole trace stream." + }, + { + "type": "text", + "title": "About: GraphQL operation rate", + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 59}, + "options": { + "mode": "markdown", + "content": "**What it shows** — the busiest GraphQL operations by request rate, from sampled traces. Good for seeing *what the customer's app is doing* and whether a particular operation surged.\n\n**What bad looks like** — one operation dominating far above normal (a client hot-loop or retry storm), or the whole stream going flat (traffic stopped).\n\n**Do next** — for *errors* by GraphQL service/operation, see the **GraphQL errors** section below. Note: spanmetrics have no namespace label, so this is not scoped to the selected install." + } + }, + { + "type": "timeseries", + "title": "GraphQL p95 latency by span (sampled)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 8, "w": 16, "x": 0, "y": 67}, + "targets": [ + { + "expr": "topk(10, histogram_quantile(0.95, sum by(le, span_name)(rate(traces_spanmetrics_duration_milliseconds_bucket{graphql_service!=\"\"}[$__rate_interval]))))", + "legendFormat": "{{span_name}}", + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "ms", "min": 0}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "right", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, + "description": "From spanmetrics (traces), which are sampled. Spanmetrics have NO namespace label, so this is not filtered by $namespace." + }, + { + "type": "text", + "title": "About: GraphQL p95 latency", + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 67}, + "options": { + "mode": "markdown", + "content": "**What it shows** — the slowest GraphQL operations at p95, in milliseconds, from sampled traces.\n\n**What bad looks like** — an operation's p95 climbing into the seconds, especially if it lines up with the customer's complaint.\n\n**Do next** — Open Traces in Explore (top-right), filter to that operation, and read the resolver chain. For error attribution by service, see the **GraphQL errors** section below." + } + }, + { + "type": "row", + "title": "GraphQL errors — attribution (traces)", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 75}, + "collapsed": false + }, + { + "type": "stat", + "title": "GraphQL errors in range (sampled)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 4, "w": 12, "x": 0, "y": 76}, + "targets": [ + { + "expr": "sum(increase(traces_spanmetrics_calls_milliseconds_total{graphql_service!=\"\", status_code=\"STATUS_CODE_ERROR\"}[$__range]))", + "instant": true, + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "short", "decimals": 0}, "overrides": []}, + "options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "graphMode": "area"} + }, + { + "type": "stat", + "title": "Distinct erroring services", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 4, "w": 12, "x": 12, "y": 76}, + "targets": [ + { + "expr": "count(count by (graphql_service) (increase(traces_spanmetrics_calls_milliseconds_total{graphql_service!=\"\", status_code=\"STATUS_CODE_ERROR\"}[$__range]) > 0))", + "instant": true, + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "short", "decimals": 0}, "overrides": []}, + "options": {"reduceOptions": {"calcs": ["lastNotNull"]}, "colorMode": "value", "graphMode": "none"} + }, + { + "type": "text", + "title": "About: Error overview", + "gridPos": {"h": 4, "w": 24, "x": 0, "y": 80}, + "options": { + "mode": "markdown", + "content": "**What it shows** — top-line GraphQL error health, from sampled traces (trust the trend, not the exact count).\n\n**What bad looks like** — *distinct erroring services* jumping from ~1 to many (a broad outage vs. one bad dependency), or the error count climbing off a flat baseline.\n\n**Do next** — one service dominating → look at **Attribution by service** below and open its trace via **Open Traces in Explore** (top-right)." + } + }, + { + "type": "bargauge", + "title": "Top services by error count", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 9, "w": 12, "x": 0, "y": 84}, + "targets": [ + { + "expr": "topk(15, sum by (graphql_service) (increase(traces_spanmetrics_calls_milliseconds_total{graphql_service!=\"\", status_code=\"STATUS_CODE_ERROR\"}[$__range])))", + "legendFormat": "{{graphql_service}}", + "instant": true, + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "short", "decimals": 0}, "overrides": []}, + "options": {"orientation": "horizontal", "displayMode": "gradient", "showUnfilled": true} + }, + { + "type": "table", + "title": "Top service × operation by errors", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 9, "w": 12, "x": 12, "y": 84}, + "targets": [ + { + "expr": "topk(50, sum by (graphql_service, graphql_operationName) (increase(traces_spanmetrics_calls_milliseconds_total{graphql_service!=\"\", status_code=\"STATUS_CODE_ERROR\"}[$__range])))", + "format": "table", + "instant": true, + "refId": "A" + } + ], + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": {"Time": true}, + "indexByName": {"graphql_service": 0, "graphql_operationName": 1, "Value": 2}, + "renameByName": {"graphql_service": "Service", "graphql_operationName": "Operation", "Value": "Errors in range"} + } + } + ], + "options": {"showHeader": true, "cellHeight": "sm", "footer": {"show": false}, "sortBy": [{"displayName": "Errors in range", "desc": true}]}, + "fieldConfig": { + "defaults": {"custom": {"align": "left", "filterable": true}}, + "overrides": [ + { + "matcher": {"id": "byName", "options": "Errors in range"}, + "properties": [{"id": "custom.cellOptions", "value": {"type": "gauge", "mode": "gradient"}}, {"id": "custom.align", "value": "right"}] + } + ] + } + }, + { + "type": "text", + "title": "About: Attribution by service", + "gridPos": {"h": 5, "w": 24, "x": 0, "y": 93}, + "options": { + "mode": "markdown", + "content": "**What it shows** — which GraphQL **service** (schema source file) and which **operation** are producing errors, from sampled traces. The `graphql.service` label rides on gorilla's spans (core PR #43519).\n\n**What bad looks like** — one service far above the rest = a single dependency/resolver in trouble; errors spread evenly across many services = something shared (DB, auth, rate-limiter).\n\n**Do next** — note the top service/operation, then **Open Traces in Explore** (top-right), filter to gorilla + `error=true` and that operation name, and read the resolver chain / error message on the actual trace. If this panel is empty, the spanmetrics `graphql.service` dimension may not be flowing yet — confirm traces reach the gateway and the collector was restarted after the dimension was added." + } + }, + { + "type": "timeseries", + "title": "GraphQL errors per minute, by service (sampled)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 9, "w": 16, "x": 0, "y": 98}, + "targets": [ + { + "expr": "sum by (graphql_service) (rate(traces_spanmetrics_calls_milliseconds_total{graphql_service!=\"\", status_code=\"STATUS_CODE_ERROR\"}[$__rate_interval])) * 60", + "legendFormat": "{{graphql_service}}", + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "short", "min": 0, "custom": {"fillOpacity": 30, "stacking": {"mode": "normal"}}}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "right", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}} + }, + { + "type": "text", + "title": "About: Errors/min by service", + "gridPos": {"h": 9, "w": 8, "x": 16, "y": 98}, + "options": { + "mode": "markdown", + "content": "**What it shows** — the same service attribution over time, stacked so you can see *when* a service started erroring and whether it correlates with a deploy.\n\n**What bad looks like** — a service's band appearing or ballooning at a specific minute.\n\n**Do next** — line up the onset time with the **Service Image Versions** panel on [W&B Field Investigation](/d/wandb-field-investigation) (a rollout?) and with the customer's report. Then drill to a trace from that window." + } + }, + { + "type": "row", + "title": "Drill down to traces", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 107}, + "collapsed": false + }, + { + "type": "text", + "title": "About: Drill-down", + "gridPos": {"h": 4, "w": 24, "x": 0, "y": 108}, + "options": { + "mode": "markdown", + "content": "**Get to the cause in three hops:** (1) identify the top service/operation above → (2) **Open Traces in Explore** (top-right), filter `service.name` to gorilla, `error=true`, and the operation → (3) read the resolver chain and error message on the actual trace. If gorilla's OTEL log export is enabled, follow **Trace → Logs** from there to the correlated log lines." + } + }, + { + "type": "row", + "title": "Operations & background (traces)", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 112}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "History-reader span latency p95 (traces, sampled)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 9, "w": 16, "x": 0, "y": 113}, + "targets": [ + { + "expr": "histogram_quantile(0.95, sum by (le, span_name) (rate(traces_spanmetrics_duration_milliseconds_bucket{span_name=~\"(?i).*(history|clickhouse|read).*\"}[$__rate_interval])))", + "legendFormat": "{{span_name}}", + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "ms", "min": 0}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "right", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, + "description": "Trace-derived (traces_spanmetrics_*, confirmed present) so it is SAMPLED — trust the shape, not exact values. The span_name regex is a best guess; if this panel is empty, run label_values(traces_spanmetrics_duration_milliseconds_bucket, span_name) and adjust the regex to the history-reader / clickhouse span names on your stack." + }, + { + "type": "text", + "title": "About: History-reader span latency", + "gridPos": {"h": 9, "w": 8, "x": 16, "y": 113}, + "options": { + "mode": "markdown", + "content": "**What it shows** — p95 duration of the read-path spans (history reader / ClickHouse query) from sampled traces, split by span. Answers \"why won't the customer's charts load?\"\n\n**What bad looks like** — one span's p95 climbing into seconds, or a new span appearing at a specific minute.\n\n**Do next** — note the slow span and the minute it started, then **Open Traces in Explore** (top-right) and read the actual span chain. If the panel is empty, the span-name regex needs adjusting (see the panel description)." + } + }, + { + "type": "timeseries", + "title": "Artifact operation latency p95 (traces, sampled)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 9, "w": 16, "x": 0, "y": 122}, + "targets": [ + { + "expr": "histogram_quantile(0.95, sum by (le, span_name) (rate(traces_spanmetrics_duration_milliseconds_bucket{span_name=~\"(?i).*artifact.*\"}[$__rate_interval])))", + "legendFormat": "{{span_name}}", + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "ms", "min": 0}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "right", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, + "description": "Trace-derived (traces_spanmetrics_*, confirmed present) so it is SAMPLED — trust the shape, not exact values. The span_name regex matches any span containing 'artifact'; if this panel is empty, run label_values(traces_spanmetrics_duration_milliseconds_bucket, span_name) and adjust the regex to the artifact span names on your stack." + }, + { + "type": "text", + "title": "About: Artifact traces", + "gridPos": {"h": 9, "w": 8, "x": 16, "y": 122}, + "options": { + "mode": "markdown", + "content": "**What it shows** — p95 latency of artifact-related spans from sampled traces, split by span. Answers \"why is the customer's artifact upload/download slow?\"\n\n**What bad looks like** — one artifact span's p95 climbing into seconds.\n\n**Do next** — to see an individual slow artifact request end to end, use the **Open Traces in Explore** link (top-right), set the service to gorilla and filter for `duration > 3s` and an artifact operation, then read the span chain. If this panel is empty, adjust the span-name regex (see the panel description)." + } + }, + { + "type": "bargauge", + "title": "Top parquet/glue spans by call count (sampled)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 9, "w": 16, "x": 0, "y": 131}, + "targets": [ + { + "expr": "topk(15, sum by (span_name) (increase(traces_spanmetrics_calls_milliseconds_total{span_name=~\".*(parquet|glue).*\"}[$__range])))", + "legendFormat": "{{span_name}}", + "instant": true, + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "short", "min": 0, "decimals": 0}, "overrides": []}, + "options": {"orientation": "horizontal", "displayMode": "gradient", "showUnfilled": true} + }, + { + "type": "text", + "title": "About: Parquet & glue spans", + "gridPos": {"h": 9, "w": 8, "x": 16, "y": 131}, + "options": { + "mode": "markdown", + "content": "**What it shows** — trace-derived (spanmetrics) call counts for spans whose name mentions parquet or glue. This is the background export/compaction work as seen from tracing.\n\n**Note** — spanmetrics are **not** namespace-scoped and are **sampled**: trust the *shape* (which span dominates), not exact counts.\n\n**Do next** — to see an actual slow/failed export, use **Open Traces in Explore** (top-right) and filter by the span name shown here. If this panel is empty, the span name pattern may differ on your stack — check `label_values(traces_spanmetrics_calls_milliseconds_total, span_name)`." + } + } + ], + "refresh": "30s", + "schemaVersion": 39, + "style": "dark", + "tags": ["wandb", "application", "api", "graphql", "observability"], + "templating": { + "list": [ + { + "name": "namespace", + "label": "W&B Namespace", + "type": "query", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "definition": "label_values(wandb_application_info, namespace)", + "query": { + "query": "label_values(wandb_application_info, namespace)", + "refId": "namespace-variable-query" + }, + "regex": "/^(?!kube-|cert-manager$|monitoring$|wandb-operators$|operator-system$|grafana$|victoria.*$|telemetry$).*$/", + "refresh": 1, + "sort": 1, + "includeAll": false, + "multi": false, + "current": {"text": "wandb", "value": "wandb"} + } + ] + }, + "time": {"from": "now-3h", "to": "now"}, + "timezone": "browser", + "title": "W&B Application", + "uid": "wandb-application", + "version": 1 +} diff --git a/deploy/telemetry/dashboards/wandb-field-investigation.json b/deploy/telemetry/dashboards/wandb-field-investigation.json new file mode 100644 index 00000000..a85c6553 --- /dev/null +++ b/deploy/telemetry/dashboards/wandb-field-investigation.json @@ -0,0 +1,324 @@ +{ + "__inputs": [ + { + "name": "DS_VICTORIAMETRICS", + "label": "VictoriaMetrics", + "type": "datasource", + "pluginId": "victoriametrics-metrics-datasource", + "pluginName": "VictoriaMetrics" + }, + { + "name": "DS_VICTORIATRACES", + "label": "VictoriaTraces", + "type": "datasource", + "pluginId": "jaeger", + "pluginName": "Jaeger" + } + ], + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": true, + "targetBlank": true, + "title": "Open Traces in Explore", + "type": "link", + "url": "/explore?panes=%7B%22A%22:%7B%22datasource%22:%22${DS_VICTORIATRACES}%22,%22queries%22:[],%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&schemaVersion=1&orgId=1" + }, + { + "asDropdown": false, + "icon": "dashboard", + "includeVars": true, + "keepTime": true, + "targetBlank": false, + "title": "W&B Managed Install Performance", + "type": "link", + "url": "/d/wandb-managed-install-performance" + }, + { + "asDropdown": true, + "icon": "dashboard", + "includeVars": true, + "keepTime": true, + "targetBlank": false, + "title": "Deep-dives", + "type": "dashboards", + "tags": ["observability"] + } + ], + "panels": [ + { + "type": "text", + "title": "", + "transparent": false, + "gridPos": {"h": 9, "w": 24, "x": 0, "y": 0}, + "options": { + "mode": "markdown", + "content": "# W&B Field Investigation — start here\n\nFirst-look triage for the W&B app. Below is the golden-signal view; the panels answer **\"are users seeing problems?\"** Once you know the symptom, jump to the matching deep-dive (also in the **Deep-dives** menu, top-right).\n\n**Symptom → dashboard**\n\n| Customer says… | Open |\n|---|---|\n| Whole app slow or erroring | [Application](/d/wandb-application) |\n| Specific queries fail / errors | [Application](/d/wandb-application) · *GraphQL errors* section |\n| Charts won't load / data slow | [Application](/d/wandb-application) · *Operations & background* section |\n| Artifact won't upload/download | [Application](/d/wandb-application) · *Operations & background* section |\n| DB/cache is the bottleneck | [Managed Install](/d/wandb-managed-install-performance) |\n| Infra host health (CPU/mem/disk) | [Managed Install](/d/wandb-managed-install-performance) |\n\n**Handoff to engineering:** capture a **Grafana snapshot** of what you're seeing (Share → Snapshot) — data ages out fast (short retention). A plain dashboard export has no data.\n\n**Shell lookups:** version `kubectl get wandb -n <ns> -o yaml` · operator logs `kubectl logs -n wandb-operators -l control-plane=controller-manager --tail=200`. Switch installs with the **W&B Namespace** dropdown." + } + }, + { + "type": "row", + "title": "Service & Versions", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 9}, + "collapsed": false + }, + { + "type": "table", + "title": "Service Image Versions", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 12, "w": 16, "x": 0, "y": 10}, + "targets": [ + { + "expr": "wandb_application_info{namespace=\"$namespace\"}", + "format": "table", + "instant": true, + "refId": "A" + } + ], + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": {"Time": true, "Value": true, "__name__": true, "job": true, "instance": true, "namespace": true, "pod": true, "container": true, "endpoint": true, "service": true}, + "indexByName": {"application_name": 0, "image": 1, "tag": 2, "digest": 3}, + "renameByName": {"application_name": "Service", "image": "Image", "tag": "Tag", "digest": "Digest"} + } + } + ], + "options": { + "showHeader": true, + "cellHeight": "md", + "footer": {"show": false} + }, + "fieldConfig": { + "defaults": {"custom": {"align": "left", "filterable": true}}, + "overrides": [] + } + }, + { + "type": "text", + "title": "About: Service Image Versions", + "gridPos": {"h": 12, "w": 8, "x": 16, "y": 10}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\nThe currently-running container image for each managed W&B service in this install, split into image / tag / digest columns.\n\n### Why it matters\n\n- **Version lag** — the tag column tells you whether a fix has shipped here yet.\n- **Mid-upgrade drift** — if `api` is on a newer tag than `parquet`, a rollout is in progress (or got stuck) and behavior may be inconsistent.\n- **Internal registry** — the image path shows whether services are pulling from your internal mirror.\n\n### How to read it\n\nOne row per managed W&B service. If a service appears with two rows, that service is mid-rollout." + } + }, + { + "type": "row", + "title": "W&B API Health", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 22}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "W&B API Request Latency (p50 / p95 / p99)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 23}, + "targets": [ + { + "expr": "histogram_quantile(0.50, sum by(le)(rate(http_server_request_duration_seconds_bucket{service_name=\"gorilla\"}[$__rate_interval])))", + "legendFormat": "p50", + "refId": "A" + }, + { + "expr": "histogram_quantile(0.95, sum by(le)(rate(http_server_request_duration_seconds_bucket{service_name=\"gorilla\"}[$__rate_interval])))", + "legendFormat": "p95", + "refId": "B" + }, + { + "expr": "histogram_quantile(0.99, sum by(le)(rate(http_server_request_duration_seconds_bucket{service_name=\"gorilla\"}[$__rate_interval])))", + "legendFormat": "p99", + "refId": "C" + } + ], + "fieldConfig": { + "defaults": {"unit": "s", "min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "tooltip": {"mode": "multi"} + }, + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "green"}, + {"color": "yellow", "value": 1}, + {"color": "red", "value": 3} + ] + } + }, + { + "type": "text", + "title": "About: W&B API Latency", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 23}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\nThe p50/p95/p99 latency of all HTTP requests served by the W&B API. Chart loads, run-table queries, the workspace, and SDK sync all flow through this histogram.\n\n### Why it matters\n\nLatency above 3 seconds sustained for more than ~5 minutes means users are feeling it. Brief spikes under load are tolerable.\n\n### How to read it\n\nThis is a single aggregate across all routes. For a per-route view of which operations are slow, see the **Slow Operations** section below." + } + }, + { + "type": "timeseries", + "title": "W&B API Request Rate by Status Code", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 33}, + "targets": [ + { + "expr": "sum by (http_response_status_code) (rate(http_server_request_duration_seconds_count{service_name=\"gorilla\"}[$__rate_interval]))", + "legendFormat": "{{http_response_status_code}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": {"unit": "reqps", "min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "text", + "title": "About: W&B API Request Rate", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 33}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\nW&B API requests per second, broken down by HTTP response status code.\n\n### Why it matters\n\n- A spike in **5xx** means the API is failing — check pod logs and resource saturation.\n- A spike in **4xx** is usually client-side: SDK version mismatch, auth issues, malformed queries.\n- A sudden drop in **2xx** to near-zero usually means traffic is no longer reaching the API (ingress / service / DNS issue).\n\n### What to do\n\nFor 5xx: `kubectl logs -n <namespace> -l app.kubernetes.io/name=*-api --tail=200`\n\nFor 4xx: confirm the SDK version. The **Service Image Versions** panel above shows the server side." + } + }, + { + "type": "row", + "title": "Ingest", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 43}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "Filestream HTTP Request Rate by Status", + "description": "TODO: empty until gorilla emits an http.route label — only `/ping` is set today, so the `file_stream` filter matches nothing. The unsampled filestream signal is DogStatsD, which needs GORILLA_STATSD_PORT>0 (currently 0).", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 44}, + "targets": [ + { + "expr": "sum by (http_response_status_code) (rate(http_server_request_duration_seconds_count{service_name=\"gorilla\", http_route=~\".*file_stream.*\"}[$__rate_interval]))", + "legendFormat": "{{http_response_status_code}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": {"unit": "reqps", "min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "text", + "title": "About: Filestream", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 44}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\nRate of HTTP requests to the filestream endpoints, broken down by response status code. Filestream is the SDK's primary ingest path — every `wandb.log()` call from a running training job ends up here.\n\n### Why it matters\n\n- A **5xx spike** means the API is rejecting writes. Active runs will see SDK warnings and data may be delayed or dropped.\n- A **4xx spike** is usually SDK clients sending malformed or unauthorized requests — often a version mismatch.\n- A sudden **drop in 2xx** means traffic stopped reaching the API (ingress, service, DNS, or pod crash).\n\n### What to do\n\nFor 5xx: check API pod logs and resource saturation (see the **W&B Managed Install Performance** dashboard for container resources)." + } + }, + { + "type": "row", + "title": "Slow Operations", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 54}, + "collapsed": false + }, + { + "type": "text", + "title": "About: Slow Operations", + "gridPos": {"h": 4, "w": 24, "x": 0, "y": 55}, + "options": { + "mode": "markdown", + "content": "Top W&B API operations (trace span names) by **slow request count** (requests taking longer than 2.5s) over the selected time range. Sourced from spanmetrics (`traces_spanmetrics_*`) because gorilla's HTTP metrics carry no route label. Use this to identify which operations are contributing most to user-visible slowness. To drill into individual slow traces, open the **Open Traces in Explore** link in the dashboard header and filter by `service.name=gorilla` and `duration>2.5s`." + } + }, + { + "type": "table", + "title": "Top Operations by Slow Request Count (>2.5s)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 12, "w": 24, "x": 0, "y": 59}, + "targets": [ + { + "expr": "topk(20, sum by (span_name) (increase(traces_spanmetrics_duration_milliseconds_count{service_name=\"gorilla\"}[$__range]) - increase(traces_spanmetrics_duration_milliseconds_bucket{service_name=\"gorilla\", le=\"2500\"}[$__range])))", + "format": "table", + "instant": true, + "refId": "A" + } + ], + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": {"Time": true}, + "indexByName": {"span_name": 0, "Value": 1}, + "renameByName": {"span_name": "Operation", "Value": "Slow requests (>2.5s) in range"} + } + } + ], + "options": { + "showHeader": true, + "cellHeight": "md", + "footer": {"show": true, "reducer": ["sum"], "fields": ["Value"]}, + "sortBy": [{"displayName": "Slow requests (>3s) in range", "desc": true}] + }, + "fieldConfig": { + "defaults": {"custom": {"align": "left", "filterable": true}}, + "overrides": [ + { + "matcher": {"id": "byName", "options": "Slow requests (>3s) in range"}, + "properties": [ + {"id": "custom.cellOptions", "value": {"type": "gauge", "mode": "gradient"}}, + {"id": "custom.align", "value": "right"} + ] + } + ] + } + } + ], + "refresh": "30s", + "schemaVersion": 39, + "style": "dark", + "tags": ["wandb", "telemetry", "field-investigation"], + "templating": { + "list": [ + { + "name": "namespace", + "label": "W&B Namespace", + "type": "query", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "definition": "label_values(wandb_application_info, namespace)", + "query": { + "query": "label_values(wandb_application_info, namespace)", + "refId": "namespace-variable-query" + }, + "regex": "/^(?!kube-|cert-manager$|monitoring$|wandb-operators$|operator-system$|grafana$|victoria.*$|telemetry$).*$/", + "refresh": 1, + "sort": 1, + "includeAll": false, + "multi": false, + "current": {"text": "wandb", "value": "wandb"} + } + ] + }, + "time": {"from": "now-3h", "to": "now"}, + "timezone": "browser", + "title": "W&B Field Investigation", + "uid": "wandb-field-investigation", + "version": 2 +} diff --git a/deploy/telemetry/dashboards/wandb-managed-install-performance.json b/deploy/telemetry/dashboards/wandb-managed-install-performance.json new file mode 100644 index 00000000..340ff97e --- /dev/null +++ b/deploy/telemetry/dashboards/wandb-managed-install-performance.json @@ -0,0 +1,579 @@ +{ + "__inputs": [ + { + "name": "DS_VICTORIAMETRICS", + "label": "VictoriaMetrics", + "type": "datasource", + "pluginId": "victoriametrics-metrics-datasource", + "pluginName": "VictoriaMetrics" + }, + { + "name": "DS_VICTORIATRACES", + "label": "VictoriaTraces", + "type": "datasource", + "pluginId": "jaeger", + "pluginName": "Jaeger" + } + ], + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": true, + "targetBlank": true, + "title": "Open Traces in Explore", + "type": "link", + "url": "/explore?panes=%7B%22A%22:%7B%22datasource%22:%22${DS_VICTORIATRACES}%22,%22queries%22:[],%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&schemaVersion=1&orgId=1" + }, + { + "asDropdown": false, + "icon": "dashboard", + "includeVars": true, + "keepTime": true, + "targetBlank": false, + "title": "W&B Field Investigation", + "type": "link", + "url": "/d/wandb-field-investigation" + } + ], + "panels": [ + { + "type": "text", + "title": "", + "gridPos": {"h": 6, "w": 24, "x": 0, "y": 0}, + "options": { + "mode": "markdown", + "content": "# W&B Managed Install Performance\n\nInfrastructure health dashboard for a W&B install. Use this dashboard to answer **\"is the underlying infrastructure that runs W&B healthy?\"** — covers all five managed components (MySQL, Redis, Kafka, SeaweedFS, ClickHouse) plus container resource utilization across the install.\n\nFor application-layer signals (W&B API latency, ingest health, slow operations) see the [W&B Field Investigation](/d/wandb-field-investigation) dashboard.\n\n**Quick lookups from the shell:**\n\n- Install size & version: `kubectl get wandb -n <namespace> -o yaml`\n- Pod restart reasons: `kubectl get pods -n <namespace> -o jsonpath='{range .items[*]}{.metadata.name}{\"\\t\"}{.status.containerStatuses[*].lastState.terminated.reason}{\\\"\\n\\\"}{end}'`\n- OOM-killed events: `kubectl get events -n <namespace> --sort-by=.lastTimestamp | grep -i oom`\n\nUse the **W&B Namespace** dropdown above to switch installs." + } + }, + { + "type": "row", + "title": "Stability", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 6}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "Container Restarts (last hour, by container)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 7}, + "targets": [ + { + "expr": "sum by (pod, container) (increase(kube_pod_container_status_restarts_total{namespace=\"$namespace\", container!=\"\", container!~\"agent|aws.*|csi.*|efs.*|external.*|kube.*|liveness.*|prometheus.*|teleport.*|gke.*\"}[1h]))", + "legendFormat": "{{pod}} / {{container}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": {"unit": "short", "min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["sum", "max"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "text", + "title": "About: Container Restarts", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 7}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\nNumber of times each container in the selected namespace restarted within the last hour.\n\n### Why it matters\n\nA container restarting more than a couple of times per hour is almost always a problem: out-of-memory kill, panic, failed health check, or stuck dependency.\n\n### What to do\n\n- `kubectl get pods -n <namespace>` — note pods with elevated RESTARTS.\n- `kubectl describe pod -n <namespace> <pod>` — look at Last State / Reason.\n- `kubectl logs -n <namespace> <pod> --previous` — last logs before the restart." + } + }, + { + "type": "timeseries", + "title": "Top Containers by CPU (% of limit)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 17}, + "targets": [ + { + "expr": "topk(15, sum by (pod, container) (rate(container_cpu_usage_seconds_total{namespace=\"$namespace\", container!=\"\", container!=\"POD\", container!~\"agent|aws.*|csi.*|efs.*|external.*|kube.*|liveness.*|prometheus.*|teleport.*|gke.*\"}[$__rate_interval])) / on (pod, container) sum by (pod, container) (kube_pod_container_resource_limits{namespace=\"$namespace\", resource=\"cpu\"}))", + "legendFormat": "{{pod}} / {{container}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": {"unit": "percentunit", "min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "text", + "title": "About: Container CPU", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 17}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\nTop 15 containers in the selected namespace by CPU usage as a fraction of the configured CPU limit (1.0 = 100% of limit). Filters out system/agent containers. Includes both W&B services and the managed infrastructure components (MySQL, Redis, Kafka, SeaweedFS, ClickHouse).\n\n### Why it matters\n\nA container sustained above 0.8 (80% of limit) will throttle frequently, slowing every request it handles. Above 1.0 means kernel-side throttling is constant.\n\n### What to do\n\nFor the configured limit, run `kubectl describe pod -n <namespace> <pod>` and look at the Limits section, or check `spec.size` in the WeightsAndBiases CR. For sustained saturation, increase the install size.\n\n*Series will be missing for containers without a configured CPU limit.*" + } + }, + { + "type": "timeseries", + "title": "Top Containers by Memory (% of limit)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 27}, + "targets": [ + { + "expr": "topk(15, sum by (pod, container) (container_memory_working_set_bytes{namespace=\"$namespace\", container!=\"\", container!=\"POD\", container!~\"agent|aws.*|csi.*|efs.*|external.*|kube.*|liveness.*|prometheus.*|teleport.*|gke.*\"}) / on (pod, container) sum by (pod, container) (kube_pod_container_resource_limits{namespace=\"$namespace\", resource=\"memory\"}))", + "legendFormat": "{{pod}} / {{container}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": {"unit": "percentunit", "min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "text", + "title": "About: Container Memory", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 27}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\nTop 15 containers in the selected namespace by working-set memory as a fraction of the configured memory limit (1.0 = 100% of limit). Working-set is the memory the kernel will not evict.\n\n### Why it matters\n\n- A line that climbs without leveling off suggests a memory leak.\n- A line that touches 1.0 means the container was OOMKilled (visible in the Container Restarts panel above).\n- A line consistently above 0.85 means the container is one bad allocation away from OOM.\n\n### What to do\n\nFor OOM debug: `kubectl get events -n <namespace> --sort-by=.lastTimestamp | grep -i oom`. For configured limits, see `spec.size` in the WeightsAndBiases CR.\n\n*Series will be missing for containers without a configured memory limit.*" + } + }, + { + "type": "row", + "title": "MySQL", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 37}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "MySQL CPU & Threads", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 38}, + "targets": [ + { + "expr": "sum by (pod, container) (rate(container_cpu_usage_seconds_total{namespace=\"$namespace\", container=~\"mysql.*|mysqld\"}[$__rate_interval]))", + "legendFormat": "CPU cores: {{pod}} / {{container}}", + "refId": "A" + }, + { + "expr": "max by (pod) (mysql_global_status_threads_connected{job=~\"$namespace/mysql-(moco|innodb)\"})", + "legendFormat": "Threads connected: {{pod}}", + "refId": "B" + }, + { + "expr": "max by (pod) (mysql_global_status_threads_running{job=~\"$namespace/mysql-(moco|innodb)\"})", + "legendFormat": "Threads running: {{pod}}", + "refId": "C" + } + ], + "fieldConfig": { + "defaults": {"min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "text", + "title": "About: MySQL CPU & Threads", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 38}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\nMySQL CPU consumption (fraction of one core) alongside connected and currently-running thread counts.\n\n### Why it matters\n\n- High CPU + high Threads Running = MySQL is the bottleneck. Slow user-facing operations.\n- Threads Connected rising while Threads Running stays low = many idle connections, often from a leaked connection pool in an app pod.\n- Threads Running near the `max_connections` ceiling = new connections will be refused.\n\n### What to do\n\nFor sustained saturation, increase the install size (`spec.size` in the WeightsAndBiases CR), which scales MySQL resources." + } + }, + { + "type": "timeseries", + "title": "MySQL Throughput & Lock Pressure", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 48}, + "targets": [ + { + "expr": "sum(rate(mysql_global_status_questions{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval]))", + "legendFormat": "questions/sec", + "refId": "A" + }, + { + "expr": "sum(rate(mysql_global_status_innodb_data_writes{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval]))", + "legendFormat": "innodb writes/sec", + "refId": "B" + }, + { + "expr": "sum(rate(mysql_global_status_innodb_row_lock_waits{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval])) or vector(0)", + "legendFormat": "row lock waits/sec", + "refId": "C" + }, + { + "expr": "max(mysql_global_status_innodb_row_lock_current_waits{job=~\"$namespace/mysql-(moco|innodb)\"}) or vector(0)", + "legendFormat": "current row lock waits", + "refId": "D" + } + ], + "fieldConfig": { + "defaults": {"min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "text", + "title": "About: MySQL Throughput & Locks", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 48}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\nFour signals on one panel:\n\n- **questions/sec** — overall query rate hitting MySQL.\n- **innodb writes/sec** — write rate, useful for distinguishing read-heavy vs write-heavy load.\n- **row lock waits/sec** — rate of new row-lock contention events. A non-zero rate is normal under load; a sustained spike indicates contention from concurrent writes to the same rows.\n- **current row lock waits** — point-in-time count of transactions blocked waiting for a row lock. Should be near zero in steady state.\n\n### What to do\n\nSustained row lock contention usually points to a problematic write pattern. Inspect the MySQL slow query log for the queries involved." + } + }, + { + "type": "timeseries", + "title": "MySQL Errors & Slow Queries", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 58}, + "targets": [ + { + "expr": "sum(rate(mysql_global_status_aborted_clients{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval])) or vector(0)", + "legendFormat": "aborted clients/sec", + "refId": "A" + }, + { + "expr": "sum(rate(mysql_global_status_aborted_connects{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval])) or vector(0)", + "legendFormat": "aborted connects/sec", + "refId": "B" + }, + { + "expr": "sum(rate(mysql_global_status_slow_queries{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval])) or vector(0)", + "legendFormat": "slow queries/sec", + "refId": "C" + } + ], + "fieldConfig": { + "defaults": {"min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "text", + "title": "About: MySQL Errors & Slow Queries", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 58}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\n- **aborted clients/sec** — clients that connected and disconnected without properly closing the connection. Often a sign of an app pod restarting under load or a network blip.\n- **aborted connects/sec** — connection attempts that failed before reaching the auth handshake. Suggests credentials drift, MySQL refusing connections at the limit, or network-layer issues.\n- **slow queries/sec** — queries crossing the `long_query_time` threshold. Sustained non-zero rate is the leading indicator of UI slowness.\n\n### What to do\n\nFor sustained slow queries, inspect the MySQL slow query log (`kubectl exec -n <namespace> -it mysql-... -- mysql -e \"SHOW VARIABLES LIKE 'slow_query%'\"` to confirm logging is on, then inspect the file)." + } + }, + { + "type": "row", + "title": "Redis", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 68}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "Redis CPU & Memory", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 69}, + "targets": [ + { + "expr": "sum by (pod) (rate(redis_cpu_user_seconds_total[$__rate_interval])) + sum by (pod) (rate(redis_cpu_sys_seconds_total[$__rate_interval]))", + "legendFormat": "CPU cores: {{pod}}", + "refId": "A" + }, + { + "expr": "max by (pod) (redis_memory_used_bytes)", + "legendFormat": "memory used: {{pod}}", + "refId": "B" + }, + { + "expr": "max by (pod) (redis_memory_max_bytes) > 0", + "legendFormat": "memory limit: {{pod}}", + "refId": "C" + } + ], + "fieldConfig": { + "defaults": {"min": 0}, + "overrides": [ + {"matcher": {"id": "byRegexp", "options": "memory.*"}, "properties": [{"id": "unit", "value": "bytes"}]} + ] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "text", + "title": "About: Redis", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 69}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\n- **CPU cores** — sum of user + system CPU consumed by each Redis instance, in cores.\n- **memory used** — current memory in use.\n- **memory limit** — `maxmemory` config if set; absent (zero/no series) means no maxmemory cap.\n\n### Why it matters\n\nRedis is on the hot path for run-state updates and SDK polling. When CPU saturates or memory approaches the maxmemory cap (and the eviction policy starts evicting), expected reads turn into cache misses, cascading into MySQL load.\n\n### What to do\n\nIf eviction is happening (see the **Redis Clients & Eviction** panel below), increase the install size or investigate why the cache is growing faster than expected." + } + }, + { + "type": "timeseries", + "title": "Redis Clients & Expired Keys", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 79}, + "targets": [ + { + "expr": "max by (pod) (redis_connected_clients)", + "legendFormat": "connected clients: {{pod}}", + "refId": "A" + }, + { + "expr": "sum by (pod) (rate(redis_expired_keys_total[$__rate_interval])) or sum by (pod) (rate(redis_db_keys_expired_total[$__rate_interval]))", + "legendFormat": "expired keys/sec: {{pod}}", + "refId": "B" + }, + { + "expr": "sum by (pod) (rate(redis_evicted_keys_total[$__rate_interval])) or vector(0)", + "legendFormat": "evicted keys/sec: {{pod}}", + "refId": "C" + } + ], + "fieldConfig": { + "defaults": {"min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "text", + "title": "About: Redis Clients & Eviction", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 79}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\n- **connected clients** — live TCP connections to each Redis instance.\n- **expired keys/sec** — natural TTL-based expirations. Normal.\n- **evicted keys/sec** — forced evictions because `maxmemory` is full. **Any non-zero rate here is a problem** — it means Redis is throwing away cached data the application expected to find.\n\n### What to do\n\nSustained eviction calls for either raising the Redis memory limit (via install size increase) or investigating why the cache is growing faster than expected. A spike in `expired_keys` without eviction is benign." + } + }, + { + "type": "row", + "title": "Kafka", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 89}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "Kafka Messages In by Topic", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 90}, + "targets": [ + { + "expr": "sum by (topic) (rate(kafka_server_brokertopicmetrics_messagesinpersec_total[$__rate_interval]))", + "legendFormat": "{{topic}}", + "refId": "A" + }, + { + "expr": "sum(kafka_controller_kafkacontroller_offlinepartitionscount)", + "legendFormat": "offline partitions (total)", + "refId": "B" + } + ], + "fieldConfig": { + "defaults": {"min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "text", + "title": "About: Kafka", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 90}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\n- **Messages in by topic** — incoming message rate per Kafka topic, in messages/sec.\n- **offline partitions** — partitions that have no leader and can't accept reads or writes. Should always be 0.\n\n### Why it matters\n\nKafka is the asynchronous backbone for run-state events, parquet-export task scheduling, and other internal queues. When Kafka throughput drops or partitions go offline, downstream consumers stall and user-visible operations slow down.\n\n### What to do\n\n- **Offline partitions > 0**: investigate managed Kafka health immediately. `kubectl get pods -n <namespace> -l weightsandbiases.apps.wandb.com/component=kafka`, inspect logs, and check that Bufstream and etcd pods are Ready.\n- **Sudden drop in messages**: check whether the producer-side services (api, filestream) are healthy in the W&B Field Investigation dashboard." + } + }, + { + "type": "row", + "title": "ClickHouse", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 100}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "ClickHouse Memory, Connections, Merges", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 101}, + "targets": [ + { + "expr": "sum(ClickHouseMetrics_MemoryTracking)", + "legendFormat": "memory tracking (bytes)", + "refId": "A" + }, + { + "expr": "sum(ClickHouseMetrics_HTTPConnection)", + "legendFormat": "HTTP connections", + "refId": "B" + }, + { + "expr": "sum(ClickHouseMetrics_Merge)", + "legendFormat": "merges in progress", + "refId": "C" + } + ], + "fieldConfig": { + "defaults": {"min": 0}, + "overrides": [ + {"matcher": {"id": "byName", "options": "memory tracking (bytes)"}, "properties": [{"id": "unit", "value": "bytes"}]} + ] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "text", + "title": "About: ClickHouse", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 101}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\n- **memory tracking** — total bytes ClickHouse is currently using across all queries.\n- **HTTP connections** — active HTTP client connections to ClickHouse.\n- **merges in progress** — background MergeTree merge operations currently running.\n\n### Why it matters\n\n- **Memory** climbing toward the configured limit signals query-side pressure. ClickHouse aborts queries when memory is exhausted, which surfaces as errors in dependent services.\n- **HTTP connections** persistently near zero may indicate clients can't reach ClickHouse.\n- **Merges** that climb without coming back down indicate ingest exceeding merge throughput; this leads to part-count limits and eventual write rejections.\n\n### What to do\n\nFor sustained memory pressure or merge backlog, increase the install size, which scales ClickHouse resources. For per-query memory diagnostics, query `system.query_log` directly on the ClickHouse pod." + } + }, + { + "type": "row", + "title": "Object Store", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 111}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "Object Store Capacity Used % & Request Rate", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 112}, + "targets": [ + { + "expr": "100 * (1 - (sum(SeaweedFS_volumeServer_resource{type=\"free\"}) / sum(SeaweedFS_volumeServer_resource{type=\"all\"})))", + "legendFormat": "capacity used %", + "refId": "A" + }, + { + "expr": "sum(rate(SeaweedFS_filer_request_total[$__rate_interval]))", + "legendFormat": "requests/sec", + "refId": "B" + } + ], + "fieldConfig": { + "defaults": {"min": 0}, + "overrides": [ + {"matcher": {"id": "byName", "options": "capacity used %"}, "properties": [{"id": "unit", "value": "percent"}, {"id": "max", "value": 100}]}, + {"matcher": {"id": "byName", "options": "requests/sec"}, "properties": [{"id": "unit", "value": "reqps"}]} + ] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "tooltip": {"mode": "multi"} + }, + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "green"}, + {"color": "yellow", "value": 75}, + {"color": "red", "value": 90} + ] + } + }, + { + "type": "text", + "title": "About: Object Store", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 112}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\n- **capacity used %** — percentage of usable volume-server capacity in use, derived from `SeaweedFS_volumeServer_resource{type=\"free\"|\"all\"}`.\n- **requests/sec** — filer HTTP request rate across all operations (`SeaweedFS_filer_request_total`).\n\n### Why it matters\n\nSeaweedFS is the object store for parquet files, artifact contents, and run media.\n\n- **Capacity above 90%** = urgent action required. New writes will fail soon.\n- **Capacity above 75%** = plan a storage increase.\n- **Sudden drop in request rate** = the API or executor services may be unable to reach the filer; check the seaweedfs Service and pod logs.\n\n### What to do\n\nFor capacity: increase storage in the WeightsAndBiases CR (`spec.objectStore.managedObjectStore.storageSize`) and re-apply, or migrate older artifacts off-cluster. For request failures: `kubectl logs -n <namespace> -l app.kubernetes.io/managed-by=seaweedfs-operator,app.kubernetes.io/component=filer --tail=200`." + } + }, + { + "type": "row", + "title": "Storage Path Diagnostics", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 122}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "Slowest Storage Operations (p95, by span)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 123}, + "targets": [ + { + "expr": "topk(10, histogram_quantile(0.95, sum by (service_name, span_name, le) (rate(traces_spanmetrics_duration_milliseconds_bucket{service_name=~\"gorilla.*\", span_name=~\"(?i)(parquet|filestream|filehandler|runstore|historystore|metadatastore|s3|sql).*\"}[$__rate_interval]))))", + "legendFormat": "{{service_name}} / {{span_name}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": {"unit": "ms", "min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["max", "mean"]}, + "tooltip": {"mode": "multi", "sort": "desc"} + } + }, + { + "type": "text", + "title": "About: Slow Storage Operations", + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 123}, + "options": { + "mode": "markdown", + "content": "### What is this?\n\np95 latency per storage operation, derived from traces by the OTel `spanmetrics` connector. Covers parquet reads/writes, filestream chunks, S3 calls, and SQL statements.\n\n### Why it matters\n\nSlow chart loads, slow file uploads, and stuck artifact downloads almost always trace back to one storage path being slow. This panel surfaces which one without opening individual traces.\n\n### What to do\n\n- Spikes on `ParquetHistoryStore.*` or `HistoryStore.*` → chart loads will feel slow. Check the parquet pod's CPU/memory in **Container Resource Usage** above, and ClickHouse load if installed.\n- Spikes on `FileStreamStore.*` / `FileHandler.*` → ingest stalls. Check filestream container restarts and object store capacity below.\n- Spikes on `sql.*` → MySQL is the bottleneck. See **MySQL Errors & Slow Queries** above.\n- For per-request detail, [open Traces in Explore](/explore?panes=%7B%22A%22:%7B%22datasource%22:%22${DS_VICTORIATRACES}%22,%22queries%22:[],%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&schemaVersion=1&orgId=1) and filter by the offending `service.name` + `span.name` with `duration > 1s`." + } + } + ], + "refresh": "30s", + "schemaVersion": 39, + "style": "dark", + "tags": ["wandb", "telemetry", "performance"], + "templating": { + "list": [ + { + "name": "namespace", + "label": "W&B Namespace", + "type": "query", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "definition": "label_values(wandb_application_info, namespace)", + "query": { + "query": "label_values(wandb_application_info, namespace)", + "refId": "namespace-variable-query" + }, + "regex": "/^(?!kube-|cert-manager$|monitoring$|wandb-operators$|operator-system$|grafana$|victoria.*$|telemetry$).*$/", + "refresh": 1, + "sort": 1, + "includeAll": false, + "multi": false, + "current": {"text": "wandb", "value": "wandb"} + } + ] + }, + "time": {"from": "now-3h", "to": "now"}, + "timezone": "browser", + "title": "W&B Managed Install Performance", + "uid": "wandb-managed-install-performance", + "version": 3 +} diff --git a/deploy/telemetry/dashboards/wandb-telemetry-overview.json b/deploy/telemetry/dashboards/wandb-telemetry-overview.json new file mode 100644 index 00000000..0367215b --- /dev/null +++ b/deploy/telemetry/dashboards/wandb-telemetry-overview.json @@ -0,0 +1,2266 @@ +{ + "__inputs": [ + { + "name": "DS_VICTORIAMETRICS", + "label": "VictoriaMetrics", + "type": "datasource", + "pluginId": "victoriametrics-metrics-datasource", + "pluginName": "VictoriaMetrics" + }, + { + "name": "DS_VICTORIATRACES", + "label": "VictoriaTraces", + "type": "datasource", + "pluginId": "jaeger", + "pluginName": "Jaeger" + } + ], + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": true, + "targetBlank": true, + "title": "Open Traces in Explore", + "type": "link", + "url": "/explore?panes=%7B%22A%22:%7B%22datasource%22:%22${DS_VICTORIATRACES}%22,%22queries%22:[],%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&schemaVersion=1&orgId=1" + } + ], + "panels": [ + { + "type": "row", + "title": "Overview", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "collapsed": false + }, + { + "type": "stat", + "title": "Scrape Success", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "targets": [ + { + "expr": "100 * avg(up)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "percent", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red" + }, + { + "color": "yellow", + "value": 90 + }, + { + "color": "green", + "value": 99 + } + ] + } + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Down Targets", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "targets": [ + { + "expr": "count(up == 0) or vector(0)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "HTTP Requests / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "targets": [ + { + "expr": "sum(rate(http_server_request_duration_seconds_count{service_name=\"gorilla\"}[$__rate_interval]))", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "API P95 Latency", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "targets": [ + { + "expr": "1000 * histogram_quantile(0.95, sum(rate(http_server_request_duration_seconds_bucket{service_name=\"gorilla\"}[$__rate_interval])) by (le))", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "ms" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "MySQL Questions / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 1 + }, + "targets": [ + { + "expr": "sum(rate(mysql_global_status_questions{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval]))", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Spans Exported / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 1 + }, + "targets": [ + { + "expr": "sum(rate(otelcol_exporter_sent_spans[$__rate_interval]))", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + + { + "type": "row", + "title": "HTTP & GraphQL", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "collapsed": false + }, + { + "type": "stat", + "title": "HTTP 4xx / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 6 + }, + "targets": [ + { + "expr": "sum(rate(http_server_request_duration_seconds_count{service_name=\"gorilla\",http_response_status_code=~\"4..\"}[$__rate_interval])) or vector(0)", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "HTTP 5xx / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 6 + }, + "targets": [ + { + "expr": "sum(rate(http_server_request_duration_seconds_count{service_name=\"gorilla\",http_response_status_code=~\"5..\"}[$__rate_interval])) or vector(0)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Avg API Latency", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 6 + }, + "targets": [ + { + "expr": "1000 * (sum(rate(http_server_request_duration_seconds_sum{service_name=\"gorilla\"}[$__rate_interval])) / sum(rate(http_server_request_duration_seconds_count{service_name=\"gorilla\"}[$__rate_interval])))", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "ms" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "GraphQL Calls / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 6 + }, + "targets": [ + { + "expr": "sum(rate(graphql_query_dist_duration_count[$__rate_interval])) or vector(0)", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Range GraphQL Calls", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 6 + }, + "targets": [ + { + "expr": "sum(increase(graphql_query_dist_duration_count[$__range])) or vector(0)", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Range Metadata Ops", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 6 + }, + "targets": [ + { + "expr": "sum(increase(operation_duration_count{operation!~\"get|set|add_to_set|delete|length_of_set\"}[$__range])) or vector(0)", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "timeseries", + "title": "Responses by Status Code", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 10 + }, + "targets": [ + { + "expr": "sum by (http_response_status_code) (rate(http_server_request_duration_seconds_count{service_name=\"gorilla\"}[$__rate_interval]))", + "legendFormat": "{{http_response_status_code}}", + "refId": "A" + } + ] + }, + { + "type": "timeseries", + "title": "Request Rate by Method", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 10 + }, + "targets": [ + { + "expr": "sum by (http_request_method) (rate(http_server_request_duration_seconds_count{service_name=\"gorilla\"}[$__rate_interval]))", + "legendFormat": "{{http_request_method}}", + "refId": "A" + } + ] + }, + { + "type": "timeseries", + "title": "GraphQL Operation Call Rate", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "targets": [ + { + "expr": "topk(10, sum(rate(graphql_query_dist_duration_count[$__rate_interval])) by (operation))", + "legendFormat": "{{operation}}", + "refId": "A" + } + ] + }, + { + "type": "timeseries", + "title": "GraphQL Avg Duration by Operation", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 18 + }, + "targets": [ + { + "expr": "1000 * topk(10, (sum(rate(graphql_query_dist_duration_sum[$__rate_interval])) by (operation)) / (sum(rate(graphql_query_dist_duration_count[$__rate_interval])) by (operation)))", + "legendFormat": "{{operation}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "ms" + }, + "overrides": [] + } + }, + + { + "type": "row", + "title": "Metadata & DB", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 26 + }, + "collapsed": false + }, + { + "type": "timeseries", + "title": "Metadata Operation Call Rate", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 27 + }, + "targets": [ + { + "expr": "topk(10, sum(rate(operation_duration_count{operation!~\"get|set|add_to_set|delete|length_of_set\"}[$__rate_interval])) by (operation))", + "legendFormat": "{{operation}}", + "refId": "A" + } + ] + }, + { + "type": "timeseries", + "title": "Metadata Avg Duration", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 27 + }, + "targets": [ + { + "expr": "1000 * topk(10, (sum(rate(operation_duration_sum{operation!~\"get|set|add_to_set|delete|length_of_set\"}[$__rate_interval])) by (operation)) / (sum(rate(operation_duration_count{operation!~\"get|set|add_to_set|delete|length_of_set\"}[$__rate_interval])) by (operation)))", + "legendFormat": "{{operation}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "ms" + }, + "overrides": [] + } + }, + { + "type": "timeseries", + "title": "MySQL Query P95 by Service", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 35 + }, + "targets": [ + { + "expr": "histogram_quantile(0.95, sum(rate(db_sql_latency_milliseconds_bucket{db_system=\"mysql\"}[$__rate_interval])) by (le, service_name))", + "legendFormat": "{{service_name}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "ms" + }, + "overrides": [] + } + }, + { + "type": "timeseries", + "title": "Redis Pool Usage by Service", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 35 + }, + "targets": [ + { + "expr": "sum(db_client_connections_usage{db_system=\"redis\"}) by (service_name)", + "legendFormat": "{{service_name}}", + "refId": "A" + } + ] + }, + { + "type": "timeseries", + "title": "Redis Pool Capacity by Service", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 43 + }, + "targets": [ + { + "expr": "sum(db_client_connections_max{db_system=\"redis\"}) by (service_name)", + "legendFormat": "{{service_name}}", + "refId": "A" + } + ] + }, + { + "type": "timeseries", + "title": "Redis Pool P95 Use Time", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 43 + }, + "targets": [ + { + "expr": "histogram_quantile(0.95, sum(rate(db_client_connections_use_time_milliseconds_bucket{db_system=\"redis\"}[$__rate_interval])) by (le, service_name))", + "legendFormat": "{{service_name}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "ms" + }, + "overrides": [] + } + }, + + { + "type": "row", + "title": "MySQL", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 51 + }, + "collapsed": false + }, + { + "type": "stat", + "title": "MySQL Exporter Up", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 52 + }, + "targets": [ + { + "expr": "max(mysql_up{job=~\"$namespace/mysql-(moco|innodb)\"})", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red" + }, + { + "color": "green", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Threads Connected", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 52 + }, + "targets": [ + { + "expr": "max(mysql_global_status_threads_connected{job=~\"$namespace/mysql-(moco|innodb)\"})", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Threads Running", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 52 + }, + "targets": [ + { + "expr": "max(mysql_global_status_threads_running{job=~\"$namespace/mysql-(moco|innodb)\"})", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "InnoDB Writes / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 52 + }, + "targets": [ + { + "expr": "sum(rate(mysql_global_status_innodb_data_writes{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval]))", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Slow Queries / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 52 + }, + "targets": [ + { + "expr": "sum(rate(mysql_global_status_slow_queries{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval])) or vector(0)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Fsync / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 52 + }, + "targets": [ + { + "expr": "sum(rate(mysql_global_status_innodb_os_log_fsyncs{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval]))", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "timeseries", + "title": "MySQL Throughput", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 56 + }, + "targets": [ + { + "expr": "sum(rate(mysql_global_status_questions{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval]))", + "legendFormat": "questions", + "refId": "A" + }, + { + "expr": "sum(rate(mysql_global_status_innodb_data_writes{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval]))", + "legendFormat": "innodb writes", + "refId": "B" + }, + { + "expr": "sum(rate(mysql_global_status_innodb_os_log_fsyncs{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval]))", + "legendFormat": "fsyncs", + "refId": "C" + } + ] + }, + { + "type": "timeseries", + "title": "MySQL Network Bytes", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 56 + }, + "targets": [ + { + "expr": "sum(rate(mysql_global_status_bytes_received{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval]))", + "legendFormat": "bytes received", + "refId": "A" + }, + { + "expr": "sum(rate(mysql_global_status_bytes_sent{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval]))", + "legendFormat": "bytes sent", + "refId": "B" + } + ], + "fieldConfig": { + "defaults": { + "unit": "Bps" + }, + "overrides": [] + } + }, + { + "type": "timeseries", + "title": "Row Lock Pressure", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 64 + }, + "targets": [ + { + "expr": "max(mysql_global_status_innodb_row_lock_current_waits{job=~\"$namespace/mysql-(moco|innodb)\"})", + "legendFormat": "current waits", + "refId": "A" + }, + { + "expr": "sum(rate(mysql_global_status_innodb_row_lock_waits{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval])) or vector(0)", + "legendFormat": "waits / sec", + "refId": "B" + } + ] + }, + { + "type": "timeseries", + "title": "MySQL Read vs Write Rate", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 64 + }, + "targets": [ + { + "expr": "sum(rate(mysql_global_status_innodb_data_reads{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval])) or vector(0)", + "legendFormat": "reads", + "refId": "A" + }, + { + "expr": "sum(rate(mysql_global_status_innodb_data_writes{job=~\"$namespace/mysql-(moco|innodb)\"}[$__rate_interval]))", + "legendFormat": "writes", + "refId": "B" + } + ] + }, + + { + "type": "row", + "title": "Redis & Caches", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 72 + }, + "collapsed": false + }, + { + "type": "stat", + "title": "Connected Clients", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 73 + }, + "targets": [ + { + "expr": "sum(redis_connected_clients)", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Redis Commands / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 73 + }, + "targets": [ + { + "expr": "sum(rate(redis_commands_processed_total[$__rate_interval]))", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Redis Errors / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 73 + }, + "targets": [ + { + "expr": "sum(rate(redis_errors_total[$__rate_interval])) or vector(0)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Redis Memory Used", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 73 + }, + "targets": [ + { + "expr": "sum(redis_memory_used_bytes)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Redis Input Bps", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 73 + }, + "targets": [ + { + "expr": "sum(rate(redis_net_input_bytes_total[$__rate_interval]))", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "Bps" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Redis Output Bps", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 73 + }, + "targets": [ + { + "expr": "sum(rate(redis_net_output_bytes_total[$__rate_interval]))", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "Bps" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "timeseries", + "title": "Redis Traffic", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 77 + }, + "targets": [ + { + "expr": "sum(rate(redis_commands_processed_total[$__rate_interval]))", + "legendFormat": "commands", + "refId": "A" + }, + { + "expr": "sum(rate(redis_commands_failed_calls_total[$__rate_interval])) or vector(0)", + "legendFormat": "failed calls", + "refId": "B" + }, + { + "expr": "sum(rate(redis_connections_received_total[$__rate_interval])) or vector(0)", + "legendFormat": "connections received", + "refId": "C" + } + ] + }, + { + "type": "timeseries", + "title": "Redis Memory Breakdown", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 77 + }, + "targets": [ + { + "expr": "sum(redis_memory_used_bytes)", + "legendFormat": "used", + "refId": "A" + }, + { + "expr": "sum(redis_memory_used_dataset_bytes)", + "legendFormat": "dataset", + "refId": "B" + }, + { + "expr": "sum(redis_memory_used_rss_bytes)", + "legendFormat": "rss", + "refId": "C" + } + ], + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + } + }, + { + "type": "timeseries", + "title": "Redis Keyspace Activity", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 85 + }, + "targets": [ + { + "expr": "sum(rate(redis_keyspace_hits_total[$__rate_interval])) or vector(0)", + "legendFormat": "hits", + "refId": "A" + }, + { + "expr": "sum(rate(redis_keyspace_misses_total[$__rate_interval])) or vector(0)", + "legendFormat": "misses", + "refId": "B" + }, + { + "expr": "sum(rate(redis_evicted_keys_total[$__rate_interval])) or vector(0)", + "legendFormat": "evicted keys", + "refId": "C" + } + ] + }, + { + "type": "timeseries", + "title": "Redis Connection Create P95 by Service", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 85 + }, + "targets": [ + { + "expr": "histogram_quantile(0.95, sum(rate(db_client_connections_create_time_milliseconds_bucket{db_system=\"redis\"}[$__rate_interval])) by (le, service_name))", + "legendFormat": "{{service_name}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "ms" + }, + "overrides": [] + } + }, + + { + "type": "row", + "title": "Workload Resources", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 93 + }, + "collapsed": false + }, + { + "type": "timeseries", + "title": "CPU by Pod", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 94 + }, + "targets": [ + { + "expr": "sum(rate(container_cpu_usage_seconds_total{namespace=\"$namespace\",pod=~\"(api|executor|filemeta|flat-run-fields-updater|glue|metric-observer|nginx-proxy|parquet|weave|weave-trace|weave-trace-worker|weave-trace-evaluate-model-worker)-.*\"}[$__rate_interval])) by (pod)", + "legendFormat": "{{pod}}", + "refId": "A" + } + ] + }, + { + "type": "timeseries", + "title": "Memory by Pod", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 94 + }, + "targets": [ + { + "expr": "sum(container_memory_working_set_bytes{namespace=\"$namespace\",pod=~\"(api|executor|filemeta|flat-run-fields-updater|glue|metric-observer|nginx-proxy|parquet|weave|weave-trace|weave-trace-worker|weave-trace-evaluate-model-worker)-.*\"}) by (pod)", + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + } + }, + + { + "type": "row", + "title": "Traces", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 103 + }, + "collapsed": false + }, + { + "type": "stat", + "title": "Spans Accepted / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 104 + }, + "targets": [ + { + "expr": "sum(rate(otelcol_receiver_accepted_spans[$__rate_interval]))", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Spans Exported / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 104 + }, + "targets": [ + { + "expr": "sum(rate(otelcol_exporter_sent_spans[$__rate_interval]))", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Failed Span Exports / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 104 + }, + "targets": [ + { + "expr": "sum(rate(otelcol_exporter_send_failed_spans[$__rate_interval])) or vector(0)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 0.1 + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "text", + "title": "Trace Coverage", + "gridPos": { + "h": 4, + "w": 12, + "x": 12, + "y": 104 + }, + "options": { + "content": "<ul><li>Use the dashboard header link or <a href=\"/explore?panes=%7B%22A%22:%7B%22datasource%22:%22${DS_VICTORIATRACES}%22,%22queries%22:[],%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&schemaVersion=1&orgId=1\" target=\"_blank\" rel=\"noopener noreferrer\"><b>open VictoriaTraces in Explore</b></a> for span search.</li><li>Current traced services typically include <code>gorilla</code>, <code>gorilla-executor</code>, <code>gorilla-filemeta</code>, <code>gorilla-flat-run-fields-updater</code>, <code>gorilla-glue</code>, <code>gorilla-metric-observer</code>, and <code>gorilla-parquet</code>.</li><li>The HTTP panels in this dashboard are keyed off <code>service_name="gorilla"</code>; deeper workload breakdown is best seen in traces and DB/pool telemetry until more services publish first-class HTTP metrics.</li></ul>", + "mode": "html" + } + }, + { + "type": "timeseries", + "title": "Span Flow", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 108 + }, + "targets": [ + { + "expr": "sum(rate(otelcol_receiver_accepted_spans[$__rate_interval]))", + "legendFormat": "accepted", + "refId": "A" + }, + { + "expr": "sum(rate(otelcol_exporter_sent_spans[$__rate_interval]))", + "legendFormat": "exported", + "refId": "B" + }, + { + "expr": "sum(rate(otelcol_exporter_send_failed_spans[$__rate_interval])) or vector(0)", + "legendFormat": "failed", + "refId": "C" + } + ] + }, + + { + "type": "row", + "title": "Storage & Infra", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 116 + }, + "collapsed": false + }, + { + "type": "stat", + "title": "ClickHouse Memory Tracking", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 117 + }, + "targets": [ + { + "expr": "sum(ClickHouseMetrics_MemoryTracking)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "ClickHouse HTTP Connections", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 117 + }, + "targets": [ + { + "expr": "sum(ClickHouseMetrics_HTTPConnection)", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Kafka Messages / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 117 + }, + "targets": [ + { + "expr": "sum(rate(kafka_server_brokertopicmetrics_messagesinpersec_total[$__rate_interval]))", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "Offline Partitions", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 117 + }, + "targets": [ + { + "expr": "sum(kafka_controller_kafkacontroller_offlinepartitionscount)", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "SeaweedFS Requests / sec", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 117 + }, + "targets": [ + { + "expr": "sum(rate(SeaweedFS_filer_request_total[$__rate_interval]))", + "refId": "A" + } + ], + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "stat", + "title": "SeaweedFS Free Capacity", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 117 + }, + "targets": [ + { + "expr": "sum(SeaweedFS_volumeServer_resource{type=\"free\"})", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + } + }, + { + "type": "timeseries", + "title": "ClickHouse Activity", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 121 + }, + "targets": [ + { + "expr": "sum(ClickHouseMetrics_Merge)", + "legendFormat": "active merges", + "refId": "A" + }, + { + "expr": "sum(ClickHouseMetrics_HTTPConnection)", + "legendFormat": "http connections", + "refId": "B" + } + ] + }, + { + "type": "timeseries", + "title": "Kafka Messages by Topic", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 121 + }, + "targets": [ + { + "expr": "sum(rate(kafka_server_brokertopicmetrics_messagesinpersec_total[$__rate_interval])) by (topic)", + "legendFormat": "{{topic}}", + "refId": "A" + } + ] + }, + { + "type": "timeseries", + "title": "SeaweedFS Capacity Used %", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 121 + }, + "targets": [ + { + "expr": "100 * (1 - (sum(SeaweedFS_volumeServer_resource{type=\"free\"}) / sum(SeaweedFS_volumeServer_resource{type=\"all\"})))", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "percent" + }, + "overrides": [] + } + }, + { + "type": "timeseries", + "title": "ClickHouse Memory Tracking", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 129 + }, + "targets": [ + { + "expr": "sum(ClickHouseMetrics_MemoryTracking)", + "legendFormat": "memory tracking", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + } + }, + { + "type": "timeseries", + "title": "SeaweedFS Request Rate", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 129 + }, + "targets": [ + { + "expr": "sum(rate(SeaweedFS_filer_request_total[$__rate_interval]))", + "legendFormat": "requests", + "refId": "A" + } + ] + }, + { + "type": "row", + "title": "Telemetry Self-Health (can I trust this data?)", + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 137}, + "collapsed": false + }, + { + "type": "timeseries", + "title": "Span Export Failure %", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 8, "w": 16, "x": 0, "y": 138}, + "targets": [ + { + "expr": "sum(rate(otelcol_exporter_send_failed_spans[$__rate_interval])) / clamp_min(sum(rate(otelcol_exporter_sent_spans[$__rate_interval])) + sum(rate(otelcol_exporter_send_failed_spans[$__rate_interval])), 1)", + "legendFormat": "failed export ratio", + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "percentunit", "min": 0}, "overrides": []}, + "options": {"legend": {"displayMode": "list", "placement": "bottom"}, "tooltip": {"mode": "multi"}}, + "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "yellow", "value": 0.01}, {"color": "red", "value": 0.05}]} + }, + { + "type": "text", + "title": "About: Telemetry Self-Health", + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 138}, + "options": { + "mode": "markdown", + "content": "### Read this FIRST when a dashboard looks wrong\n\n**What it shows** — the fraction of trace spans the collector failed to export to storage. If this is non-zero, the trace/RED panels across every dashboard are **undercounting** — the data itself is lossy, not the app.\n\n**What bad looks like** — sustained > ~1%. Above ~5% treat trace-derived numbers as unreliable.\n\n**Do next** — if failures are high, check the `victoria-otlp-gateway` collector pod and the VictoriaTraces backend before trusting any trace panel. Also cross-check **Scrape Success** / **Down Targets** at the top of this dashboard for metric-side gaps." + } + }, + { + "type": "timeseries", + "title": "Trace ingestion by receiver (DataDog / OTLP)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 8, "w": 16, "x": 0, "y": 146}, + "targets": [ + { + "expr": "sum by (receiver) (rate(otelcol_receiver_accepted_spans[$__rate_interval]))", + "legendFormat": "{{receiver}}", + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "cps", "min": 0}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi"}}, + "description": "Spans/sec the collector accepts, split by receiver. The `datadog` receiver (port 8126) is the DataDog-APM path — background/SDK services (weave-trace, glue, parquet, filemeta, metric-observer, anaconda2) send here; `otlp` is the main gorilla trace path." + }, + { + "type": "text", + "title": "About: Ingestion by receiver", + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 146}, + "options": { + "mode": "markdown", + "content": "**What it shows** — whether each ingestion path on the gateway is receiving data. This is how you confirm **DataDog** traces/metrics are populated.\n\n- **`datadog`** — DataDog-APM traces (port 8126). A non-zero line here means DataDog-protocol tracing is flowing; downstream it appears as `traces_spanmetrics_calls_total` for the SDK services.\n- **`otlp`** — the main gorilla OpenTelemetry path (the bulk of spans/metrics).\n- **`statsd`** — DogStatsD metrics (port 8125). **Expect this to be absent/zero** unless gorilla runs with `GORILLA_STATSD_PORT` > 0 (a core/manifest-side setting). Zero here is the reason the DogStatsD-only panels were dropped.\n\n**What bad looks like** — a receiver you expect traffic on flatlining at 0, or `otelcol_receiver_refused_*` climbing (data arriving but rejected)." + } + }, + { + "type": "timeseries", + "title": "Metric-point ingestion by receiver (DogStatsD / OTLP)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 8, "w": 16, "x": 0, "y": 154}, + "targets": [ + { + "expr": "sum by (receiver) (rate(otelcol_receiver_accepted_metric_points[$__rate_interval]))", + "legendFormat": "{{receiver}}", + "refId": "A" + } + ], + "fieldConfig": {"defaults": {"unit": "cps", "min": 0}, "overrides": []}, + "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi"}}, + "description": "Metric points/sec the collector accepts, split by receiver. A `statsd` line appears only when gorilla emits DogStatsD (GORILLA_STATSD_PORT > 0); today only `otlp` carries metrics." + }, + { + "type": "text", + "title": "About: Receiver errors", + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 154}, + "options": { + "mode": "markdown", + "content": "**Pairs with the panel to the left.** If a receiver shows accepted traffic but data still isn't landing in dashboards, check the refused counters:\n\n- `sum by (receiver) (rate(otelcol_receiver_refused_spans[$__rate_interval]))`\n- `sum by (receiver) (rate(otelcol_receiver_refused_metric_points[$__rate_interval]))`\n\n**Accepted > 0, refused ≈ 0** = healthy ingestion. **Refused climbing** = the gateway is receiving but rejecting (bad payloads, queue full) — check the `victoria-otlp-gateway` collector logs." + } + } + ], + "refresh": "30s", + "schemaVersion": 39, + "style": "dark", + "tags": [ + "wandb", + "telemetry", + "observability" + ], + "templating": { + "list": [ + { + "name": "namespace", + "label": "W&B Namespace", + "type": "query", + "datasource": { + "type": "victoriametrics-metrics-datasource", + "uid": "${DS_VICTORIAMETRICS}" + }, + "definition": "label_values(wandb_application_info, namespace)", + "query": { + "query": "label_values(wandb_application_info, namespace)", + "refId": "namespace-variable-query" + }, + "refresh": 1, + "sort": 1, + "includeAll": false, + "multi": false, + "current": { + "text": "wandb", + "value": "wandb" + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timezone": "browser", + "title": "W&B Telemetry Overview", + "uid": "wandb-telemetry-overview", + "version": 3 +} diff --git a/deploy/telemetry/templates/_helpers.tpl b/deploy/telemetry/templates/_helpers.tpl new file mode 100644 index 00000000..282bb721 --- /dev/null +++ b/deploy/telemetry/templates/_helpers.tpl @@ -0,0 +1,59 @@ +{{- define "telemetry.namespace" -}} +{{- if .Values.namespace -}} +{{- .Values.namespace -}} +{{- else if .Release.Namespace -}} +{{- .Release.Namespace -}} +{{- else -}} +{{- .Release.Namespace -}} +{{- end -}} +{{- end -}} + +{{- define "telemetry.stackEnabled" -}} +{{- $mode := default "off" .Values.mode -}} +{{- if ne $mode "off" -}}true{{- else -}}false{{- end -}} +{{- end -}} + +{{- define "telemetry.uiEnabled" -}} +{{- $mode := default "off" .Values.mode -}} +{{- if eq $mode "full" -}}true{{- else -}}false{{- end -}} +{{- end -}} + +{{- define "telemetry.forwardingEnabled" -}} +{{- if and (eq (default "off" .Values.mode) "forward") .Values.forwarding.otlp.endpoint -}}true{{- else -}}false{{- end -}} +{{- end -}} + +{{- define "telemetry.forwardingExporterName" -}} +{{- if eq (default "http/protobuf" .Values.forwarding.otlp.protocol) "grpc" -}}otlp/external{{- else -}}otlphttp/external{{- end -}} +{{- end -}} + +{{- define "telemetry.vmsingleName" -}} +victoria-instance +{{- end -}} + +{{- define "telemetry.vmagentName" -}} +victoria-agent +{{- end -}} + +{{- define "telemetry.vlsingleName" -}} +victoria-logs +{{- end -}} + +{{- define "telemetry.vtsingleName" -}} +victoria-traces +{{- end -}} + +{{- define "telemetry.otlpGatewayName" -}} +victoria-otlp-gateway +{{- end -}} + +{{- define "telemetry.metricsEndpoint" -}} +{{- printf "http://vmsingle-%s:8428/opentelemetry/v1/metrics" (include "telemetry.vmsingleName" .) -}} +{{- end -}} + +{{- define "telemetry.logsEndpoint" -}} +{{- printf "http://vlsingle-%s:9428/insert/opentelemetry/v1/logs" (include "telemetry.vlsingleName" .) -}} +{{- end -}} + +{{- define "telemetry.tracesEndpoint" -}} +{{- printf "http://vtsingle-%s:10428/insert/opentelemetry/v1/traces" (include "telemetry.vtsingleName" .) -}} +{{- end -}} diff --git a/deploy/telemetry/templates/telemetry-alerting.yaml b/deploy/telemetry/templates/telemetry-alerting.yaml new file mode 100644 index 00000000..4ddb6a70 --- /dev/null +++ b/deploy/telemetry/templates/telemetry-alerting.yaml @@ -0,0 +1,58 @@ +{{- if and (eq (include "telemetry.stackEnabled" .) "true") .Values.alerting.enabled }} +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMRule +metadata: + name: wandb-telemetry-default-rules + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb + telemetry.wandb.com/rule-group: default +spec: + groups: + - name: wandb-telemetry + interval: {{ .Values.alerting.evaluationInterval | quote }} + rules: + - alert: WandbOperatorDown + expr: up{job=~".*wandb-operator.*"} == 0 + for: 5m + labels: + severity: warning + annotations: + summary: W&B operator metrics target is down + description: VictoriaMetrics cannot scrape the W&B operator metrics endpoint. + - alert: WandbNoKafkaMetrics + expr: absent(up{job=~".*kafka.*"}) + for: 10m + labels: + severity: warning + annotations: + summary: Kafka metrics are missing + description: No Kafka metrics targets were discovered for at least 10 minutes. +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMAlert +metadata: + name: wandb-vmalert + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + datasource: + url: {{ printf "http://vmsingle-%s:8428" (include "telemetry.vmsingleName" .) | quote }} + remoteRead: + url: {{ printf "http://vmsingle-%s:8428" (include "telemetry.vmsingleName" .) | quote }} + remoteWrite: + url: {{ printf "http://vmsingle-%s:8428/api/v1/write" (include "telemetry.vmsingleName" .) | quote }} + evaluationInterval: {{ .Values.alerting.evaluationInterval | quote }} + selectAllByDefault: false + ruleSelector: + matchLabels: + telemetry.wandb.com/rule-group: default + {{- if .Values.alerting.notifier.enabled }} + notifiers: + - url: {{ .Values.alerting.notifier.target | quote }} + {{- end }} +{{- end }} diff --git a/deploy/telemetry/templates/telemetry-otlp-gateway.yaml b/deploy/telemetry/templates/telemetry-otlp-gateway.yaml new file mode 100644 index 00000000..50533b87 --- /dev/null +++ b/deploy/telemetry/templates/telemetry-otlp-gateway.yaml @@ -0,0 +1,220 @@ +{{- if eq (include "telemetry.stackEnabled" .) "true" }} +{{- $forwardingEnabled := eq (include "telemetry.forwardingEnabled" .) "true" }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "telemetry.otlpGatewayName" . }}-config + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "telemetry.otlpGatewayName" . }} + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +data: + collector.yaml: | + receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + statsd/dogstatsd: + endpoint: 0.0.0.0:8125 + aggregation_interval: 15s + # Map DogStatsD timing/histogram/distribution metrics to histograms so + # percentiles (p50/p95/p99) are queryable. Without this the receiver emits + # summaries (count/sum only) and gorilla's operation.duration, dist_*, and + # *_dist_duration families lose their tails — leaving only averages. + timer_histogram_mapping: + - statsd_type: timing + observer_type: histogram + histogram: + max_size: 100 + - statsd_type: histogram + observer_type: histogram + histogram: + max_size: 100 + - statsd_type: distribution + observer_type: histogram + histogram: + max_size: 100 + datadog: + endpoint: 0.0.0.0:8126 + + processors: + batch: {} + + connectors: + # Derives RED-style metrics (request count + duration histogram) from + # incoming traces. service.name and span.name are implicit identity. + # Dimensions promote span attributes to metric labels for breakdowns: + # - http.route: HTTP route. + # - graphql.service / graphql.operationName: gorilla sets these on its + # OTEL spans (core PR #43519), enabling per-service GraphQL error + # attribution from metrics. graphql.service is bounded (~schema source + # files); graphql.operationName is higher-cardinality — watch the + # ~50k-series budget and drop it to trace-only if threatened. + # user.id / project.name / entity.name remain deferred (too high-card). + spanmetrics: + namespace: traces_spanmetrics + dimensions: + - name: http.route + default: "" + - name: graphql.service + default: "" + - name: graphql.operationName + default: "" + # Attach trace IDs to metric datapoints so a latency/error spike links + # straight to an example trace. + exemplars: + enabled: true + histogram: + explicit: + buckets: [5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s] + metrics_flush_interval: 15s + + exporters: + otlphttp/vm: + endpoint: http://vmsingle-{{ include "telemetry.vmsingleName" . }}:8428 + metrics_endpoint: http://vmsingle-{{ include "telemetry.vmsingleName" . }}:8428/opentelemetry/v1/metrics + otlphttp/vl: + endpoint: http://vlsingle-{{ include "telemetry.vlsingleName" . }}:9428 + logs_endpoint: http://vlsingle-{{ include "telemetry.vlsingleName" . }}:9428/insert/opentelemetry/v1/logs + otlphttp/vt: + endpoint: http://vtsingle-{{ include "telemetry.vtsingleName" . }}:10428 + traces_endpoint: http://vtsingle-{{ include "telemetry.vtsingleName" . }}:10428/insert/opentelemetry/v1/traces +{{ if $forwardingEnabled }} +{{ if eq (default "http/protobuf" .Values.forwarding.otlp.protocol) "grpc" }} + otlp/external: + endpoint: {{ .Values.forwarding.otlp.endpoint | quote }} + tls: + insecure: {{ .Values.forwarding.otlp.tls.insecure }} +{{ with .Values.forwarding.otlp.headers }} + headers: +{{ toYaml . | nindent 10 }} +{{ end }} +{{ else }} + otlphttp/external: + endpoint: {{ .Values.forwarding.otlp.endpoint | quote }} +{{ with .Values.forwarding.otlp.headers }} + headers: +{{ toYaml . | nindent 10 }} +{{ end }} +{{ end }} +{{ end }} + + service: + telemetry: + metrics: + address: 0.0.0.0:8888 + pipelines: + metrics: + receivers: [otlp, statsd/dogstatsd, spanmetrics] + processors: [batch] + exporters: [otlphttp/vm{{ if $forwardingEnabled }}, {{ include "telemetry.forwardingExporterName" . }}{{ end }}] + logs: + receivers: [otlp] + processors: [batch] + exporters: [otlphttp/vl{{ if $forwardingEnabled }}, {{ include "telemetry.forwardingExporterName" . }}{{ end }}] + traces: + receivers: [otlp, datadog] + processors: [batch] + exporters: [otlphttp/vt, spanmetrics{{ if $forwardingEnabled }}, {{ include "telemetry.forwardingExporterName" . }}{{ end }}] +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "telemetry.otlpGatewayName" . }} + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "telemetry.otlpGatewayName" . }} + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: {{ include "telemetry.otlpGatewayName" . }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "telemetry.otlpGatewayName" . }} + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb + spec: + {{- with .Values.otlpGateway.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: otel-collector + image: "otel/opentelemetry-collector-contrib:0.102.1" + imagePullPolicy: IfNotPresent + {{- with .Values.otlpGateway.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + args: + - --config=/etc/otelcol/collector.yaml + ports: + - name: otlp-grpc + containerPort: 4317 + protocol: TCP + - name: otlp-http + containerPort: 4318 + protocol: TCP + - name: statsd + containerPort: 8125 + protocol: UDP + - name: datadog-trace + containerPort: 8126 + protocol: TCP + - name: metrics + containerPort: 8888 + protocol: TCP + volumeMounts: + - name: config + mountPath: /etc/otelcol + volumes: + - name: config + configMap: + name: {{ include "telemetry.otlpGatewayName" . }}-config + items: + - key: collector.yaml + path: collector.yaml +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "telemetry.otlpGatewayName" . }} + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/name: {{ include "telemetry.otlpGatewayName" . }} + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + app.kubernetes.io/name: {{ include "telemetry.otlpGatewayName" . }} + ports: + - name: otlp-grpc + port: 4317 + targetPort: otlp-grpc + protocol: TCP + - name: otlp-http + port: 4318 + targetPort: otlp-http + protocol: TCP + - name: statsd + port: 8125 + targetPort: statsd + protocol: UDP + - name: datadog-trace + port: 8126 + targetPort: datadog-trace + protocol: TCP + - name: metrics + port: 8888 + targetPort: metrics + protocol: TCP +{{- end }} diff --git a/deploy/telemetry/templates/telemetry-scrapes.yaml b/deploy/telemetry/templates/telemetry-scrapes.yaml new file mode 100644 index 00000000..d074c773 --- /dev/null +++ b/deploy/telemetry/templates/telemetry-scrapes.yaml @@ -0,0 +1,403 @@ +{{- if and (eq (include "telemetry.stackEnabled" .) "true") .Values.scrape.enabled }} +{{- if .Values.scrape.kubeletCadvisor }} +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMNodeScrape +metadata: + name: kubelet-cadvisor + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + scheme: https + tlsConfig: + insecureSkipVerify: true + caFile: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + relabelConfigs: + - action: labelmap + regex: __meta_kubernetes_node_label_(.+) + - targetLabel: __address__ + replacement: kubernetes.default.svc:443 + - sourceLabels: [__meta_kubernetes_node_name] + regex: (.+) + targetLabel: __metrics_path__ + replacement: /api/v1/nodes/$1/proxy/metrics/cadvisor +{{- end }} + +{{- if .Values.scrape.operators }} +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMServiceScrape +metadata: + name: wandb-operator + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + app.kubernetes.io/name: wandb-operator + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + endpoints: + # The operator emits `wandb_application_info{namespace="<wandb-cr-ns>"}` + # where `namespace` is the W&B CR's namespace (what dashboards filter on), + # not the operator pod's namespace. honorLabels keeps the metric's label + # instead of letting target-side `namespace=wandb-operators` overwrite it. + - port: metrics + scheme: http + honorLabels: true +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMServiceScrape +metadata: + name: clickhouse-operator + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + app.kubernetes.io/name: altinity-clickhouse-operator + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + endpoints: + - port: op-metrics +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMServiceScrape +metadata: + name: grafana-operator + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + app.kubernetes.io/name: grafana-operator + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + endpoints: + - port: metrics +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMServiceScrape +metadata: + name: victoria-metrics-operator + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + app.kubernetes.io/name: victoria-metrics-operator + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + endpoints: + - port: http +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMServiceScrape +metadata: + name: otlp-gateway + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + app.kubernetes.io/name: {{ include "telemetry.otlpGatewayName" . }} + namespaceSelector: + matchNames: + - {{ include "telemetry.namespace" . }} + endpoints: + - port: metrics +{{- end }} + +{{- if and (eq (include "telemetry.uiEnabled" .) "true") .Values.scrape.kubeStateMetrics }} +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMServiceScrape +metadata: + name: kube-state-metrics + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + app.kubernetes.io/name: kube-state-metrics + namespaceSelector: + any: true + endpoints: + # KSM exposes its own pod's namespace/pod labels by default. honorLabels + # keeps the metric's intrinsic labels (kube_pod_container_*'s namespace + # refers to the *target* pod, which is what dashboards filter on) instead + # of letting the target-side namespace overwrite them. + - port: http + scheme: http + honorLabels: true +{{- end }} + +{{- if .Values.scrape.wandbApi }} +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: wandb-api + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchExpressions: + - key: app.kubernetes.io/name + operator: Exists + namespaceSelector: + matchNames: + - {{ include "telemetry.namespace" . }} + podMetricsEndpoints: + - path: /metrics + relabelConfigs: + - sourceLabels: + - __meta_kubernetes_pod_label_app_kubernetes_io_name + regex: .*-api + action: keep + - sourceLabels: + - __meta_kubernetes_pod_ip + regex: (.+) + replacement: $1:8181 + targetLabel: __address__ + action: replace +{{- end }} + +{{- if .Values.scrape.infrastructure }} +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: mysql-pxc + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + app.kubernetes.io/component: pxc + namespaceSelector: + matchNames: + - {{ include "telemetry.namespace" . }} + podMetricsEndpoints: + - port: metrics + path: /metrics +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: mysql-proxysql + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + app.kubernetes.io/component: proxysql + namespaceSelector: + matchNames: + - {{ include "telemetry.namespace" . }} + podMetricsEndpoints: + - port: stats + path: /metrics +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: mysql-innodb + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + component: mysqld + mysql.oracle.com/instance-type: group-member + namespaceSelector: + matchNames: + - {{ include "telemetry.namespace" . }} + podMetricsEndpoints: + - port: metrics + path: /metrics +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: mysql-moco + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + app.kubernetes.io/created-by: moco + app.kubernetes.io/name: mysql + namespaceSelector: + matchNames: + - {{ include "telemetry.namespace" . }} + podMetricsEndpoints: + - port: mysqld-metrics + path: /metrics +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: kafka-brokers + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + weightsandbiases.apps.wandb.com/component: kafka + namespaceSelector: + matchNames: + - {{ include "telemetry.namespace" . }} + podMetricsEndpoints: + - port: metrics + path: /metrics +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: seaweedfs-master + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + app.kubernetes.io/managed-by: seaweedfs-operator + app.kubernetes.io/component: master + namespaceSelector: + matchNames: + - {{ include "telemetry.namespace" . }} + podMetricsEndpoints: + - port: master-metrics + path: /metrics +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: seaweedfs-volume + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + app.kubernetes.io/managed-by: seaweedfs-operator + app.kubernetes.io/component: volume + namespaceSelector: + matchNames: + - {{ include "telemetry.namespace" . }} + podMetricsEndpoints: + - port: volume-metrics + path: /metrics +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: seaweedfs-filer + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + app.kubernetes.io/managed-by: seaweedfs-operator + app.kubernetes.io/component: filer + namespaceSelector: + matchNames: + - {{ include "telemetry.namespace" . }} + podMetricsEndpoints: + - port: filer-metrics + path: /metrics +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: redis + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + role: standalone + namespaceSelector: + matchNames: + - {{ include "telemetry.namespace" . }} + podMetricsEndpoints: + - port: redis-exporter + path: /metrics +--- +apiVersion: v1 +kind: Service +metadata: + name: clickhouse-metrics + namespace: {{ include "telemetry.namespace" . }} + labels: + app: clickhouse-metrics + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + weightsandbiases.apps.wandb.com/component: clickhouse + ports: + - name: metrics + port: 9363 + targetPort: 9363 + protocol: TCP + clusterIP: None +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMServiceScrape +metadata: + name: clickhouse + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selector: + matchLabels: + app: clickhouse-metrics + namespaceSelector: + matchNames: + - {{ include "telemetry.namespace" . }} + endpoints: + - port: metrics + path: /metrics +{{- end }} +{{- end }} diff --git a/deploy/telemetry/templates/telemetry-ui.yaml b/deploy/telemetry/templates/telemetry-ui.yaml new file mode 100644 index 00000000..58550846 --- /dev/null +++ b/deploy/telemetry/templates/telemetry-ui.yaml @@ -0,0 +1,199 @@ +{{- if eq (include "telemetry.uiEnabled" .) "true" }} +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: Grafana +metadata: + name: grafana + namespace: {{ include "telemetry.namespace" . }} + labels: + dashboards: grafana + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + config: + log: + mode: console + auth: + disable_login_form: "true" + auth.anonymous: + enabled: "true" + org_role: Admin + preferences: + homeDashboardUid: wandb-telemetry-overview + deployment: + spec: + template: + spec: + containers: + - name: grafana + env: + - name: GF_INSTALL_PLUGINS + value: victoriametrics-metrics-datasource,victoriametrics-logs-datasource +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDatasource +metadata: + name: victoria-metrics + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + instanceSelector: + matchLabels: + dashboards: grafana + datasource: + name: VictoriaMetrics + uid: victoriametrics + type: victoriametrics-metrics-datasource + access: proxy + url: {{ include "telemetry.metricsEndpoint" . | trimSuffix "/opentelemetry/v1/metrics" | quote }} + isDefault: true + jsonData: + timeInterval: 30s + # Pivot from a metric exemplar (spanmetrics attaches trace IDs) to the trace. + exemplarTraceIdDestinations: + - name: trace_id + datasourceUid: victoriatraces + urlDisplayLabel: View trace +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDatasource +metadata: + name: victoria-logs + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + instanceSelector: + matchLabels: + dashboards: grafana + datasource: + name: VictoriaLogs + uid: victorialogs + type: victoriametrics-logs-datasource + access: proxy + url: {{ include "telemetry.logsEndpoint" . | trimSuffix "/insert/opentelemetry/v1/logs" | quote }} + jsonData: + maxLines: 1000 +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDatasource +metadata: + name: victoria-traces + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + instanceSelector: + matchLabels: + dashboards: grafana + datasource: + name: VictoriaTraces + uid: victoriatraces + type: jaeger + access: proxy + url: {{ printf "%s/select/jaeger" (include "telemetry.tracesEndpoint" . | trimSuffix "/insert/opentelemetry/v1/traces") | quote }} + jsonData: + # Pivot from a trace to its logs in VictoriaLogs, correlating on service name. + tracesToLogsV2: + datasourceUid: victorialogs + spanStartTimeShift: "-1h" + spanEndTimeShift: "1h" + filterByTraceID: false + tags: + - key: service.name + value: service +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + name: wandb-telemetry-overview + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + uid: wandb-telemetry-overview + folder: W&B + instanceSelector: + matchLabels: + dashboards: grafana + datasources: + - inputName: DS_VICTORIAMETRICS + datasourceName: VictoriaMetrics + - inputName: DS_VICTORIATRACES + datasourceName: VictoriaTraces + json: |- +{{ .Files.Get "dashboards/wandb-telemetry-overview.json" | nindent 4 }} +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + name: wandb-field-investigation + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + uid: wandb-field-investigation + folder: W&B + instanceSelector: + matchLabels: + dashboards: grafana + datasources: + - inputName: DS_VICTORIAMETRICS + datasourceName: VictoriaMetrics + - inputName: DS_VICTORIATRACES + datasourceName: VictoriaTraces + json: |- +{{ .Files.Get "dashboards/wandb-field-investigation.json" | nindent 4 }} +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + name: wandb-managed-install-performance + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + uid: wandb-managed-install-performance + folder: W&B + instanceSelector: + matchLabels: + dashboards: grafana + datasources: + - inputName: DS_VICTORIAMETRICS + datasourceName: VictoriaMetrics + - inputName: DS_VICTORIATRACES + datasourceName: VictoriaTraces + json: |- +{{ .Files.Get "dashboards/wandb-managed-install-performance.json" | nindent 4 }} +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDashboard +metadata: + name: wandb-application + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + uid: wandb-application + folder: W&B + instanceSelector: + matchLabels: + dashboards: grafana + datasources: + - inputName: DS_VICTORIAMETRICS + datasourceName: VictoriaMetrics + - inputName: DS_VICTORIATRACES + datasourceName: VictoriaTraces + - inputName: DS_VICTORIALOGS + datasourceName: VictoriaLogs + json: |- +{{ .Files.Get "dashboards/wandb-application.json" | nindent 4 }} +{{- end }} diff --git a/deploy/telemetry/templates/telemetry-victoria-core.yaml b/deploy/telemetry/templates/telemetry-victoria-core.yaml new file mode 100644 index 00000000..3821cb4a --- /dev/null +++ b/deploy/telemetry/templates/telemetry-victoria-core.yaml @@ -0,0 +1,50 @@ +{{- if eq (include "telemetry.stackEnabled" .) "true" }} +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMSingle +metadata: + name: {{ include "telemetry.vmsingleName" . }} + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + retentionPeriod: {{ .Values.retentionPeriod | quote }} + extraArgs: + opentelemetry.usePrometheusNaming: "true" +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMAgent +metadata: + name: {{ include "telemetry.vmagentName" . }} + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + selectAllByDefault: true + remoteWrite: + - url: {{ printf "http://vmsingle-%s:8428/api/v1/write" (include "telemetry.vmsingleName" .) | quote }} +--- +apiVersion: operator.victoriametrics.com/v1 +kind: VLSingle +metadata: + name: {{ include "telemetry.vlsingleName" . }} + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + retentionPeriod: {{ .Values.retentionPeriod | quote }} +--- +apiVersion: operator.victoriametrics.com/v1 +kind: VTSingle +metadata: + name: {{ include "telemetry.vtsingleName" . }} + namespace: {{ include "telemetry.namespace" . }} + labels: + app.kubernetes.io/component: telemetry + app.kubernetes.io/part-of: wandb +spec: + retentionPeriod: {{ .Values.retentionPeriod | quote }} +{{- end }} diff --git a/deploy/telemetry/templates/validation.yaml b/deploy/telemetry/templates/validation.yaml new file mode 100644 index 00000000..9f401a0e --- /dev/null +++ b/deploy/telemetry/templates/validation.yaml @@ -0,0 +1,4 @@ +{{- $mode := default "off" .Values.mode -}} +{{- if and (eq $mode "forward") (eq (default "" .Values.forwarding.otlp.endpoint) "") -}} +{{- fail "mode=forward requires forwarding.otlp.endpoint" -}} +{{- end -}} diff --git a/deploy/telemetry/values.schema.json b/deploy/telemetry/values.schema.json new file mode 100644 index 00000000..2ac49ac1 --- /dev/null +++ b/deploy/telemetry/values.schema.json @@ -0,0 +1,124 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true, + "properties": { + "mode": { + "type": "string", + "enum": ["off", "forward", "full"] + }, + "namespace": { + "type": "string" + }, + "forwarding": { + "type": "object", + "additionalProperties": false, + "properties": { + "otlp": { + "type": "object", + "additionalProperties": false, + "properties": { + "endpoint": { + "type": "string" + }, + "protocol": { + "type": "string", + "enum": ["http/protobuf", "grpc"] + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "tls": { + "type": "object", + "additionalProperties": false, + "properties": { + "insecure": { + "type": "boolean" + } + } + } + } + } + } + }, + "retentionPeriod": { + "type": "string", + "minLength": 1 + }, + "otlpGateway": { + "type": "object", + "additionalProperties": false, + "properties": { + "podSecurityContext": { + "type": "object", + "additionalProperties": true + }, + "securityContext": { + "type": "object", + "additionalProperties": true + } + } + }, + "scrape": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "kubeletCadvisor": { + "type": "boolean" + }, + "operators": { + "type": "boolean" + }, + "wandbApi": { + "type": "boolean" + }, + "infrastructure": { + "type": "boolean" + }, + "kubeStateMetrics": { + "type": "boolean" + } + } + }, + "alerting": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "evaluationInterval": { + "type": "string", + "minLength": 1 + }, + "notifier": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string", + "minLength": 1 + }, + "type": { + "type": "string", + "enum": ["webhook", "email", "slack"] + }, + "target": { + "type": "string", + "minLength": 1 + } + } + } + } + } + } +} diff --git a/deploy/telemetry/values.yaml b/deploy/telemetry/values.yaml new file mode 100644 index 00000000..e4cce5a9 --- /dev/null +++ b/deploy/telemetry/values.yaml @@ -0,0 +1,37 @@ +mode: "off" +namespace: "" +forwarding: + otlp: + endpoint: "" + protocol: http/protobuf + headers: {} + tls: + insecure: false +retentionPeriod: 1d +otlpGateway: + podSecurityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + runAsNonRoot: true +scrape: + enabled: true + kubeletCadvisor: true + operators: true + wandbApi: true + infrastructure: true + kubeStateMetrics: true +alerting: + enabled: false + evaluationInterval: 30s + notifier: + enabled: false + name: default + type: webhook + target: http://alertmanager-operated.monitoring.svc:9093 diff --git a/docs/design/wandb_v2/infra_reconciliation.md b/docs/design/wandb_v2/infra_reconciliation.md new file mode 100644 index 00000000..8b322e40 --- /dev/null +++ b/docs/design/wandb_v2/infra_reconciliation.md @@ -0,0 +1,108 @@ +# Infra V2 Reconciliation Flows + +Given that we're entering a WandB V2 Reconciliation, here is how the process flows from a coarse-grained +perspective followed by narrower and more detailed views. + +The WandB V2 Custom Resource controls third-party infra CR's. + +```mermaid +graph TD + WandB[WandB CR v2] + + WandB -->|controls| Kafka[Strimzi Kafka CR] + WandB -->|controls| Clickhouse[Altinity Clickhouse CR] + WandB -->|controls| SeaweedFS[SeaweedFS CR] + WandB -->|controls| Redis[Opstree Redis CR] + WandB -->|controls| Mysql[Percona Mysql CR] + + style Kafka fill:#DAA520 + style Clickhouse fill:#DAA520 + style SeaweedFS fill:#DAA520 + style Redis fill:#DAA520 + style Mysql fill:#DAA520 +``` + +The infra CR's may have non-standard or otherwise varying behavior. As a result, some WandB reconciliation +with those CR's may also vary and are noted here: + +* Clickhouse +* Kafka + +## Top-level Flow + +```mermaid +flowchart TD + Flagged{in deletion?} + + Flagged -->|yes| Finalize + Flagged -->|no| Write + + subgraph Finalize[Finalize] + FKafka[Kafka] + FCH[CH] + FSeaweedFS[SeaweedFS] + FRedis[Redis] + FMysql[Mysql] + end + + subgraph Write[Write Infra State] + WKafka[Kafka] + WCH[CH] + WSeaweedFS[SeaweedFS] + WRedis[Redis] + WMysql[Mysql] + end + + Finalize -.-> Write + Write -.-> Read + + subgraph Read[Read Infra State] + RKafka[Kafka] + RCH[CH] + RSeaweedFS[SeaweedFS] + RRedis[Redis] + RMysql[Mysql] + end + + subgraph Infer[Infer WandB Component Status] + IKafka[Kafka] + ICH[CH] + ISeaweedFS[SeaweedFS] + IRedis[Redis] + IMysql[Mysql] + end + + Read -.-> Infer + + subgraph Summarize[Summarize WandB Status] + end + + Infer -.-> Summarize + + style FKafka fill:#DAA520 + style FCH fill:#DAA520 + style FSeaweedFS fill:#DAA520 + style FRedis fill:#DAA520 + style FMysql fill:#DAA520 + style WKafka fill:#DAA520 + style WCH fill:#DAA520 + style WSeaweedFS fill:#DAA520 + style WRedis fill:#DAA520 + style WMysql fill:#DAA520 + style RKafka fill:#DAA520 + style RCH fill:#DAA520 + style RSeaweedFS fill:#DAA520 + style RRedis fill:#DAA520 + style RMysql fill:#DAA520 + style IKafka fill:#DAA520 + style ICH fill:#DAA520 + style ISeaweedFS fill:#DAA520 + style IRedis fill:#DAA520 + style IMysql fill:#DAA520 + style Finalize fill:#D3D3D3 + style Read fill:#D3D3D3 + style Write fill:#D3D3D3 + style Infer fill:#D3D3D3 + style Summarize fill:#D3D3D3 +``` + diff --git a/docs/design/wandb_v2/legacy_overrides.md b/docs/design/wandb_v2/legacy_overrides.md new file mode 100644 index 00000000..c0f7a857 --- /dev/null +++ b/docs/design/wandb_v2/legacy_overrides.md @@ -0,0 +1,379 @@ +# Legacy Overrides: carrying v1 helm values into v2 + +## Problem + +The v1 `WeightsAndBiases` spec is untyped: `spec.values` is an arbitrary map passed to +the `operator-wandb` helm chart. Many v1 configurations have no strongly-typed home in +the v2 spec — most importantly: + +- **global env vars** (`global.env`, `global.extraEnv`) applied to every application, +- **per-application env vars** (`<app>.env`, `<app>.extraEnv`), +- **per-application resource overrides** (`<app>.resources`, `<app>.sizing.<size>.resources`). + +Until these get first-class v2 fields, we carry them through conversion in +`spec.wandb.legacyOverrides` (added in commit `8a344a7`) and apply them during +reconcile so a converted install keeps its effective v1 configuration. + +```go +// api/v2/weightsandbiases_types.go +LegacyOverrides map[string]LegacyOverrides `json:"legacyOverrides,omitempty"` // on WandbAppSpec + +type LegacyOverrides struct { + Env []corev1.EnvVar `json:"env,omitempty"` + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` +} +``` + +Map keys are **v2 manifest application names**, plus the reserved key **`global`** +(env only) which applies to every application. The server manifest is the +authority on which keys are valid: sections that don't correspond to a manifest +application are logged at reconcile time and never applied — but for now they are +left in place in the spec (see *Manifest validation*). + +## How v1 (the helm chart) actually behaves + +Facts established from `wandb-base/templates/_containers.tpl` and verified with +`helm template` renders; the conversion and reconcile semantics below are derived +from them. + +- Every app key in `operator-wandb/values.yaml` is an aliased instance of the + `wandb-base` library chart, so all apps support the same env/resources keys. +- Env layers are **maps** keyed by env var name; values are either scalars + (string-coerced) or full `EnvVar` bodies (`valueFrom:` supported). Precedence + (highest → lowest), collapsing to one entry per name: + 1. `<app>.containers.<n>.env` + 2. `<app>.env` + 3. `<app>.extraEnv` + 4. `global.env` + 5. `global.extraEnv` + 6. `sizing.<size>.env` + 7. chart-computed `envTpls` (an entry is *removed* when any layer above defines the same name) + 8. `envFrom` config maps/secrets +- **User env at any layer beats chart-computed env.** This is the load-bearing + behavior the v2 reconcile must reproduce: overrides must win against + manifest-provided env. +- Resources merge deep, per-field: `containers.<n>.resources` > `<app>.resources` + (legacy flat key) > `sizing.<effective size>.resources`, where effective size is + `coalesce(<app>.size, global.size, "small")` and the size entry is overlaid onto + `sizing.default`. +- All env layers reach main containers, init containers, and Job/CronJob pods. + `Values.resources`/sizing resources reach main containers only. + +## Design + +The server manifest is the single authority on which applications exist — +there is no hardcoded list of helm app keys anywhere. Conversion resolves the +manifest itself: `mapVersion` runs first and derives `spec.wandb.version` from +`app.image.tag`/`api.image.tag`, and the manifest for that version is an +immutable artifact fetched through the same `manifest.GetServerManifest` +resolver the reconciler uses (OCI via ORAS, or `file://`). This keeps +conversion stateless in the way that matters: it remains a pure function of +(values, version) — the manifest is just versioned static data. Successful +fetches need no extra caching (the resolver is local-first: once a version is +in its on-disk ORAS store, no network is involved); only *failures* are +remembered in-process for a minute, because the store retries the remote on +every call and a conversion webhook stalling for the fetch timeout (15 s) on +every v1 write would make an unreachable registry very painful. + +Reconcile-time validation still exists as a second line — it guards +hand-edited v2 CRs and version drift — mirroring the existing split for +external-infra literals (`mapMySQL` → pending annotation → +`migrateLegacyAnnotations`). + +### Phase 1: Conversion (v1 → v2) + +A new mapper `mapLegacyOverrides(values, dst)` runs in `applyValueMappings` +(`api/v1/weightsandbiases_conversion_mapping.go`) alongside the other +peer-of-global mappers (it reads top-level app keys, like `mapVersion` does). +It is pure extraction — stateless, no client — over the `resolveValues()` output, +so the spec-active Secret's coalesced values are preferred, same as every other +mapper. + +**Manifest-driven extraction.** Conversion resolves the server manifest for the +converted version and iterates `manifest.Applications`: for each application it +reads the top-level values section named by the application's **`legacyKey`** +when the manifest sets one (renamed helm aliases, e.g. `nginx-proxy` declares +`legacyKey: nginx`, `weave-trace-evaluate-model-worker` declares +`legacyKey: weave-evaluate-model-worker`), else by the application name — and +extracts its env/extraEnv/resources. The operator carries **no rename table**: +even the helm-key knowledge ships with the manifest (`legacyKey` is set +upstream in the wandb/core server-manifest generator; a manifest that predates +the field simply leaves renamed sections unmapped, logged like any other). +Only manifest applications are ever copied, so the spec never carries junk +keys, and a new application in a future manifest needs no conversion change. + +Sections that carry the override shape we extract (`env`/`extraEnv`/`sizing`) +but match no manifest application — the v1 monolith `app`, `console`, +`history-updater`, `mcp-server`, … — are **logged and skipped** at conversion. +(The flat `resources` key is deliberately not part of that detection signature: +infra subchart sections like `mysql` legitimately carry it and would be false +positives.) There is **no `app` → `api` translation**: the monolith's overrides +were tuned for a different binary, and grafting them onto v2's `api` risks more +than it fixes. Skipped sections stay recoverable in the +`legacy.operator.wandb.com/v1-values` annotation. + +**Failure containment.** Manifest resolution is best-effort and never fails +conversion: a fetch error (offline cluster, version with no published manifest) +would otherwise make v1 objects unservable and v1 writes impossible. On error — +or when values yield no version at all — only the per-application extraction is +skipped, with a log; global env still converts, and a later re-apply (once the +manifest is reachable) re-extracts everything. + +**Env extraction.** + +- `legacyOverrides["global"].Env` = `merge(global.env over global.extraEnv)`. +- `legacyOverrides[<key>].Env` = `merge(<key>.env over <key>.extraEnv)`. +- Map-shaped values decode into `corev1.EnvVar` via JSON round-trip (name from the + map key); malformed bodies fail conversion with a `spec.values.<path>` error, + matching existing mapper behavior. Scalars go through the existing + `scalarToString` (bools/numbers become strings, as helm's `toString` did). +- Entries whose string value contains `{{` are **dropped with a log line, never a + conversion failure**: they are helm template expressions we cannot evaluate, + and failing conversion would block serving the object over one env var. + Kubernetes `$(VAR)` interpolation passes through untouched. +- Env slices are **sorted by name** so conversion is deterministic and the + v2 → v1 → v2 round-trip is idempotent (required by `TestConvertRoundTrip`). + +**Resources extraction.** Per candidate key, deep-merge exactly what the user set — +`sizing.default.resources`, then `sizing.<effective size>.resources`, then +`<key>.resources` — into one `ResourceRequirements`. We extract only fragments +present in the resolved values (user values + release-channel defaults; chart +defaults never appear there), so an install that never touched resources converts +with no resource overrides and v2 manifest sizing applies untouched. No resources +are extracted for `global` (the chart has no global resources). + +`ConvertFrom` needs **no reverse mapping** — it already restores v1 purely from +the v1-values annotation. + +### Phase 2: Manifest validation at reconcile (log-only, no pruning for now) + +`validateLegacyOverrides(ctx, wandb, manifest)` runs in the v2 reconcile +immediately after the server manifest is resolved and before +`reconcileApplications`. With conversion already filtering against the +manifest, this is a second line of defense for hand-edited v2 CRs and for +version drift (the spec's overrides were extracted against one manifest +version; the reconciler may be running another): + +- Valid keys are the reserved `"global"` plus any name in + `manifest.Applications`. Validity is judged against the full application map, + **not** the feature-filtered set — an override for a feature-gated app is + valid and takes effect if the feature is enabled. +- Every other key is logged at Warn once per reconcile pass: `legacy override + section %q does not map to any application in server manifest %s; ignoring`. +- **The spec is not modified.** Unmapped keys stay in + `spec.wandb.legacyOverrides`; they are simply never applied, because the apply + path only ever looks up `"global"` and manifest application names. Pruning the + spec was considered and deferred — we can revisit once migration behavior has + been observed in the field. + +### Phase 3: Applying overrides during reconcile + +All changes are on the `WeightsAndBiases` side (`internal/controller/reconciler/`); +the `Application` controller copies pod templates verbatim and needs no changes. +`reconcileApplications`, `resolveContainers`, and `ResolveResources` already +receive the full `*v2.WeightsAndBiases`, so no plumbing is required. The apply +path looks up only `"global"` and the current app's manifest name, so unmapped +keys left in the spec are inert here by construction. + +**Env.** In `reconcileApplications` (`reconcile_v2.go`), after all existing env +construction (manifest env, telemetry injection, custom-CA injection) and +immediately before `resolveContainers`: + +```go +envVars = overrideEnvVars(envVars, wandb.Spec.Wandb.LegacyOverrides["global"].Env) +envVars = overrideEnvVars(envVars, wandb.Spec.Wandb.LegacyOverrides[app.Name].Env) +``` + +`overrideEnvVars(base, overrides)` is a new helper: replace-by-name in place, +append when missing — the inverse of `appendMissingEnvVars` (existing wins), which +cannot be reused. Ordering: global first, per-app second, so per-app beats global — +matching the chart's layer precedence. Applying last means overrides beat +manifest/common env, telemetry defaults, and CA env — exactly as user env beat +chart-computed `envTpls` in v1. Because the env slice is shared, init containers +receive the same overrides, which also matches v1. + +**Resources.** In `ResolveResources` (`sizing.go`), overlay the per-app override as +the final merge step, after the container-level merge: + +```go +if lo, ok := wandb.Spec.Wandb.LegacyOverrides[app.Name]; ok && lo.Resources != nil { + resources = mergeResources(resources, lo.Resources, wandb.Spec.RequireLimits) +} +``` + +The overlay respects `spec.requireLimits`, like every other merge in +`ResolveResources`: when `requireLimits=false` (the default), only the requests +from a legacy override are applied and its limits are stripped. This keeps the +v2 no-limits-by-default policy uniform across sizing- and legacy-derived +resources; converted requests are preserved either way, and setting +`requireLimits: true` re-enables the converted limits. (Deliberate divergence +from v1, where configured limits were always enforced — to be reevaluated.) +`ResolveResources` runs per main container only — init containers get no +`Values.resources`/sizing merge, same as v1. The `global` entry's `Resources` is +ignored (documented; there is no v1 analog). + +**Migrations.** The `global` env entry is also applied (same helper) to migration +task env in `runMigrations`, since v1's global env reached job pods too — the +canonical use case is `HTTP_PROXY`/`NO_PROXY`, which migrations need as much as +the apps do. Per-app entries do not apply to migrations (no v1 analog). + +**Hand-authored content.** Editing `legacyOverrides` directly in a v2 CR is +discouraged but not prevented — and we assume it will happen. So there is no +blocking webhook validation beyond the CRD schema, the field's doc comment (and +`docs/config-api.md`) steer users toward first-class fields, and the apply path +is defensive: entries with an empty name are skipped with a log, +`overrideEnvVars` resolves duplicate names within an override deterministically +(last entry wins), and unknown map keys are already logged and ignored by +manifest validation. Invalid env var names or resource quantities the CRD schema +can't catch surface as Deployment create/update errors on the owned +`Application`, same as any other bad pod-template input. + +### Precedence summary (v2, after this change) + +Highest → lowest for an application's container env: + +1. `legacyOverrides[<app>].env` +2. `legacyOverrides["global"].env` +3. custom-CA / telemetry injected env (append-if-missing passes) +4. manifest `app.Env` +5. manifest `CommonEnvs` groups + +Resources, per main container: `legacyOverrides[<app>].resources` > +manifest container resources > manifest `sizing[spec.size]` > manifest +`sizing.default`. + +### Flow + +```mermaid +flowchart LR + subgraph v1["v1 CR (untyped)"] + V[spec.values<br/>global.env / extraEnv<br/>app.env / resources / sizing] + end + subgraph conv["Conversion webhook (api/v1)"] + RV[resolveValues<br/>spec-active Secret or spec.values] --> MLO[mapLegacyOverrides<br/>fetch manifest for mapVersion's version<br/>iterate manifest apps via legacyKey/name<br/>env map → EnvVar list + sizing merge<br/>log + skip unmapped sections] + end + subgraph rec["v2 reconcile"] + GM[GetServerManifest] --> PR[validateLegacyOverrides<br/>log keys not in<br/>manifest.Applications] + PR --> ENV[resolveEnvvars + telemetry + CA] --> OV[overrideEnvVars<br/>global then per-app] --> RC[resolveContainers] + PR --> RR[ResolveResources<br/>+ legacy overlay] --> RC + end + V --> RV + MLO --> LO["spec.wandb.legacyOverrides"] + LO --> PR +``` + +## Out of scope (dropped, preserved only in the v1-values annotation) + +- `<app>.containers.<n>.env|resources` (per-container overrides) +- `envFrom` (configMapRef/secretRef maps), `envTpls`, `global.extraEnvFrom` +- helm-template-valued env entries (`{{ ... }}`) +- `sizing.<size>.env` (t-shirt env vars; the v2 manifest owns these) +- infra subchart keys (`mysql`, `redis`, `kafka`, `clickhouse`, …) — already + mapped to typed fields by the other conversion mappers, or genuinely not apps + +Sections for apps with no v2 counterpart (`app`, `console`, `history-updater`, +`mcp-server`, job/hook keys, …) are logged and skipped at conversion — they +never enter the spec. + +## Implementation plan + +### Step 0 — API cleanup (before anything references the field) + +1. `api/v2/weightsandbiases_types.go`: rename Go field `LegacyOveriddes` → + `LegacyOverrides` (JSON tag already correct, so this is API-compatible); + change `Resources` to `*corev1.ResourceRequirements` (`omitempty` is a no-op on + struct values, and presence must be distinguishable); add doc comments — + including an explicit note that the field is populated by v1→v2 conversion + and hand-editing is discouraged in favor of first-class fields. (This repo + generates CRDs with `maxDescLen=0`, so the comments serve godoc/readers of + the types, not the CRD schema.) +2. `make manifests generate sync-crd-embed`. + +### Step 1 — Conversion + +3. New `api/v1/weightsandbiases_conversion_overrides.go`: + - `mapLegacyOverrides(values map[string]interface{}, dst *appsv2.WeightsAndBiases) error`, + registered in `applyValueMappings` after `mapIngress` (so `mapVersion` has + already derived the version). + - `legacyManifestApps`: resolves the manifest via + `manifest.GetServerManifest` (repository = the shared + `appsv2.DefaultManifestRepository` constant, also used by the defaulting + webhook) with a per-(repository, version) failure cooldown — successes are + already cached on disk by the resolver's ORAS store — and a + `SetConversionManifestGetter` test seam; failures skip per-app extraction + with a log. + - `LegacyKey` field on the manifest `Application` type (set upstream by the + wandb/core server-manifest generator; also added to the local dev + manifests under `hack/testing-manifests/server-manifest/`), plus helpers + following existing idioms (`unstructured.Nested*`, errors prefixed + `spec.values.<path>`): env over extraEnv merge, scalar coercion, strict + EnvVar-body decode, `{{` skip, name-sorted output; resources = sizing + default → effective size → flat `resources` merge, using + `coalesce(<app>.size, global.size, "small")`; unmapped-section logging + keyed on the `env`/`extraEnv`/`sizing` shape. +4. Tests in `api/v1/weightsandbiases_conversion_overrides_test.go` (plain Go + + `require`, `newV1(values)` fixtures, fake manifest getter installed by a + package `TestMain` so unit tests never fetch over the network, + `withConversionReader` for spec-active Secret cases): global/per-app + extraction and env/extraEnv precedence, scalar coercion, `valueFrom` bodies, + template-string drop, renames, unmapped sections skipped, resources/size + selection, manifest-unavailable and no-version fallbacks, per-version fetch + caching, round-trip idempotency, spec-active Secret preference. + +### Step 2 — Manifest validation + +5. `validateLegacyOverrides` in `internal/controller/reconciler/` (new file + `legacy_overrides.go`), called from the v2 reconcile after + `GetServerManifest`, before `reconcileApplications`; Warn log per unmapped + key; **no spec mutation, no CR update**. +6. Tests (in-memory manifest, no client needed): valid keys accepted, `global` + always accepted, unmapped keys reported, feature-gated apps accepted, + spec unchanged afterward. + +### Step 3 — Applying overrides + +7. `overrideEnvVars` helper + unit tests (including hand-authored edge cases: + duplicate names within an override → last wins; empty-name entries skipped + with a log). +8. Apply global + per-app env in `reconcileApplications` before + `resolveContainers`; apply global env in `runMigrations`. +9. Legacy overlay in `ResolveResources` (+ `RequireLimits` behavior tests in + `reconcile_v2_sizing_test.go` style). +10. Tests: plain unit tests in package `reconciler` (fake client only where + `resolveEnvvars` is exercised), covering the precedence table above. + +### Step 4 — Verification & docs + +11. `make lint && make test`. +12. Add a v1 fixture with env/resources overrides (including one deliberately + unmapped section, e.g. `console`) under `hack/testing-manifests/wandb/` and + verify end-to-end via Tilt/Kind: create the v1 CR, confirm `legacyOverrides` + on the stored v2 object, confirm the unmapped section is logged and remains + in the spec without affecting any Deployment, confirm env and resources land + on the rendered Deployments, confirm + round-trip (`kubectl get wandb.v1... -o yaml` still shows original values). +13. User-facing documentation lives in the API type's doc comments and this + design doc. (`docs/config-api.md` turned out to be v1 console-API docs — the + wrong venue; revisit if a v2 CR reference doc is added.) + +## Resolved design decisions + +| Decision | Choice | Rationale | +|---|---|---| +| Authority on valid app names | the server manifest, resolved *during conversion* (and re-checked at reconcile) | no hardcoded app list to rot; the manifest is immutable versioned data, so fetching it keeps conversion a pure function of (values, version) | +| Manifest fetch failure / no version | skip per-app extraction with a log, never fail conversion; global env still converts | erroring would make v1 objects unservable for offline clusters or versions with no published manifest | +| Unmapped sections | logged and skipped at conversion; reconcile re-checks spec keys (hand-edits, version drift) and leaves them in place | visibility without destructive spec edits; only manifest apps ever enter the spec | +| helm `app` (monolith) overrides | not translated to `api`; unmapped, so logged and skipped | monolith env/resources were tuned for a different binary; grafting them onto v2 `api` causes more problems than it solves | +| Renamed helm keys | `legacyKey` field on the manifest `Application`, set upstream | no rename table in the operator; the manifest owns all application knowledge, helm aliases included | +| Global env representation | reserved `"global"` map key, merged at reconcile | keeps CR small; apps added by newer manifests still inherit it | +| Override vs manifest env | overrides win (replace-by-name) | mirrors v1, where user env at any layer displaced chart-computed env | +| Legacy limits vs `requireLimits=false` | respect `requireLimits`: limits stripped unless it's true | keeps the v2 no-limits-by-default policy uniform; reevaluate later if migrated installs need their v1 limits back | +| Resources: replace or merge | deep-merge overlay per field | mirrors helm's deep merge (verified: overriding `requests.cpu` kept chart limits) | +| Malformed env bodies | fail conversion | consistent with `classifyValueFromOrLiteral` and other mappers | +| Helm-templated env values (`{{ ... }}`) | log and drop, never fail conversion | can't be evaluated outside helm; failing would block serving the object over one env var | +| Hand-editing `legacyOverrides` | discouraged, not prevented; assumed to happen | no blocking webhook validation; doc comments + docs steer to first-class fields; apply path is defensive (empty names skipped, duplicates last-wins, unknown keys logged + ignored) | +| Reverse (v2→v1) mapping | none | `ConvertFrom` restores from the v1-values annotation already | + +## Open questions + +None — all design decisions are resolved above. diff --git a/docs/design/wandb_v2/struct_usage_graph.md b/docs/design/wandb_v2/struct_usage_graph.md new file mode 100644 index 00000000..883a2305 --- /dev/null +++ b/docs/design/wandb_v2/struct_usage_graph.md @@ -0,0 +1,214 @@ +# Struct Usage Graph v2 + +This document visualizes struct usage across the wandb operator codebase with clear separation between struct definitions (data) and code that operates on them (logic). + +## Node Shape Legend + +- **Rectangles** [Rectangle] - **Struct definitions** (data types) +- **Rounded rectangles** (Rounded) - **Code/Logic** that operates on structs (functions, controllers, files) + +## Line Style Legend + +- **Dotted lines** (⋯→) - **READ** operations (reading struct fields) +- **Solid lines** (→) - **WRITE** operations (creating/modifying structs) + +## Color Legend + +- 🔵 **Blue** (`#e1f5ff`) - API struct types (api/v2) +- 🟡 **Yellow** (`#fff4e1`) - Model struct types (internal/model) +- 🟣 **Purple** (`#ffe1f5`) - Vendored operator struct types +- 🟢 **Green** (`#e1ffe8`) - Controller/Translator code (internal/controller) +- 🌱 **Light Green** (`#f0ffe1`) - Infrastructure controller code + +--- + +## High-Level Overview + +This diagram shows the major components separating struct definitions from code that operates on them. + +```mermaid +graph TB + subgraph "Struct Definitions" + APIStructs["api/v2<br/>WeightsAndBiases types"] + ModelStructs["internal/model<br/>Config & Status types"] + VendorStructs["Vendored Operator<br/>Redis/Kafka/MySQL/MinIO/ClickHouse types"] + end + + subgraph "Code/Logic" + Controller("internal/controller/wandb_v2<br/>Main Reconciler") + Translator("internal/controller/translator/v2<br/>THE BRIDGE<br/>Defaults & Merging") + Model("internal/model<br/>Business Logic Code<br/>NO api/v2 import") + Infra("internal/controller/infra<br/>Infrastructure Controllers") + end + + Controller -->|Read & Write Status| APIStructs + + Translator -->|Read & Write Defaults| APIStructs + + Model -->|Create Size-based| ModelStructs + + Translator -.->|Read| ModelStructs + Translator -->|Convert to Status| APIStructs + + Infra -->|Create| VendorStructs + Infra -->|Read & Create Status| ModelStructs + + style APIStructs fill:#e1f5ff + style ModelStructs fill:#fff4e1 + style VendorStructs fill:#ffe1f5 + style Controller fill:#e1ffe8 + style Translator fill:#e1ffe8 + style Model fill:#e1ffe8 + style Infra fill:#f0ffe1 +``` + +--- + +## Key Insights + +### Data vs Logic Separation + +The architecture maintains clear separation: + +1. **api/v2**: Kubernetes API types (user-facing struct definitions) +2. **internal/controller/translator/v2**: THE BRIDGE - translates between api/v2 and model +3. **internal/model**: Pure business logic (NO api/v2 dependency) +4. **internal/controller/infra**: Infrastructure controllers (model → vendor CRs) +5. **Vendored operators**: External operator CRs (struct definitions only) + +### Critical Architectural Principle + +**`internal/model` does NOT import `api/v2`** - The translator layer is the ONLY bridge between them. + +This design allows `internal/model` to be reused without dragging in Kubernetes dependencies. + +### Flow Pattern + +1. **User Input** (api/v2) → **Translator** (THE BRIDGE) → **API Structs with defaults** +2. **Translator** → **Model Code** (NO api/v2 access) → **Model Structs** (Size-based configs) +3. **Model Structs** → **Infra Code** → **Vendor Structs** +4. **Vendor Status** → **Infra Code** → **Model Status** → **Translator** → **API Status** + +Key principles: +- **Data** flows through rectangles (struct definitions) +- **Transformations** happen in rounded rectangles (code/logic) +- **Reads** are dotted lines (consumption) +- **Writes** are solid lines (creation/mutation) +- **Translator is the bridge** - model does NOT import api/v2 + +--- + +## Summary Statistics + +### Struct Counts by Package + +- **API Structs (api/v2)**: 38 types +- **Model Structs (internal/model)**: 44 types +- **Vendored Operator Structs**: 261 types total + - Percona MySQL Operator: 48 types + - Redis Operator (Opstree): 27 types + - Strimzi Kafka Operator: 37 types + - MinIO Operator: 26 types + - Altinity ClickHouse Operator: 123 types + +### Infrastructure Components + +All 5 infrastructure types follow the same pattern: +1. **MySQL** - Using Percona XtraDB Cluster Operator +2. **Redis** - Using Opstree Redis Operator (standalone + sentinel modes) +3. **Kafka** - Using Strimzi Operator (KRaft mode with node pools) +4. **MinIO** - Using MinIO Operator (tenant-based) +5. **ClickHouse** - Using Altinity ClickHouse Operator + +--- + +## Viewing Instructions + +To view this diagram: + +1. **GitHub**: GitHub natively renders Mermaid diagrams in markdown files +2. **VS Code**: Install the "Markdown Preview Mermaid Support" extension +3. **Online**: Copy diagram code to https://mermaid.live/ +4. **IntelliJ/GoLand**: Built-in Mermaid support in markdown preview +5. **CLI**: Use `mmdc` (mermaid-cli) to generate PNG/SVG: + ```bash + npm install -g @mermaid-js/mermaid-cli + mmdc -i struct_usage_graph.md -o struct_usage_graph.png + ``` + +--- + +## Updating Instructions + +### When to Update + +Update this document when: +- New packages are added or major architectural changes occur +- The relationship between api/v2, translator, model, or infra changes +- New infrastructure types are added +- The separation of concerns principles change + +### How to Update + +Follow these steps to regenerate the diagram: + +1. **Verify architectural principles** (these are critical): + - Confirm `internal/model` does NOT import `api/v2` (check imports) + - Confirm `internal/controller/translator/v2` is THE BRIDGE between api/v2 and model + - Identify any new code/logic packages or struct definition packages + +2. **Identify the major components**: + - **Struct Definitions** (rectangles): api/v2, internal/model, vendored operators + - **Code/Logic** (rounded rectangles): Controller, Translator, Model, Infra + +3. **Map the data flow** (arrows): + - Start from user input (api/v2 structs) + - Follow through Controller → Translator → Model → Infra → Vendors + - Trace status flow back: Vendors → Infra → Model → Translator → api/v2 + +4. **Apply arrow rules** (IMPORTANT): + - **Solid lines** (→): Use when code WRITES to structs (even if it also reads) + - **Dotted lines** (⋯→): Use ONLY for purely READ-ONLY operations (no writes) + - **Rule**: If there's a write operation, use solid line only (don't add a dotted read line) + - Label arrows clearly: "Create", "Read & Write", "Convert to Status", etc. + +5. **Verify critical rules**: + - ✅ NO arrows from Model to APIStructs (model doesn't import api/v2) + - ✅ Translator is the ONLY bridge between APIStructs and ModelStructs + - ✅ Only ONE dotted line should exist: Translator reading ModelStructs (read-only) + - ✅ All other relationships involve writes, so use solid lines + +6. **Update statistics**: Recount structs if new types were added + +### Reference Commands + +```bash +# Verify internal/model does NOT import api/v2 (should return nothing) +grep -n "github.com/wandb/operator/api/v2" internal/common/*.go + +# Find all struct definitions in a package +grep -rn "^type.*struct {" api/v2/ +grep -rn "^type.*struct {" internal/common/ + +# Find where a struct is instantiated (writes) +grep -rn "MySQLConfig{" internal/ + +# Find code that reads from a package +grep -rn "apiv2\." internal/controller/translator/ + +# Count struct types in a package +grep -c "^type.*struct {" api/v2/*.go +``` + +### Key Architectural Principles to Preserve + +1. **internal/model is decoupled from api/v2**: No imports, no direct references +2. **Translator is THE BRIDGE**: Only translator converts between api/v2 and model types +3. **Clean separation**: Struct definitions (rectangles) vs Code/Logic (rounded rectangles) +4. **Single responsibility**: Each layer has a clear purpose +5. **Simplified diagram**: High-level overview only, no detailed per-infrastructure diagrams + +### Source Document + +For detailed struct usage mapping, see: +- `struct_usage_mapping.md` - Text-based mapping of all struct definitions and usage diff --git a/docs/design/wandb_v2/struct_usage_mapping.md b/docs/design/wandb_v2/struct_usage_mapping.md new file mode 100644 index 00000000..66d46b4a --- /dev/null +++ b/docs/design/wandb_v2/struct_usage_mapping.md @@ -0,0 +1,1143 @@ +# Struct Usage Mapping + +This document maps struct definitions and their usage (reads/writes) across the wandb operator codebase. + +## Legend +- 📦 **Package**: Where structs are defined +- 📝 **Defined**: Struct definition location +- 📖 **Read**: Package/file that reads struct fields +- ✍️ **Write**: Package/file that writes/modifies struct fields or creates instances + +--- + +## 1. API Structs (api/v2/) + +### Core CR Types + +#### WeightsAndBiases +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:41` +- 📖 **Read by**: + - `internal/controller/wandb_v2/weightsandbiases_v2_controller.go` - Reads entire resource in reconciler + - `internal/controller/wandb_v2/*.go` - All infrastructure reconcilers read spec fields + - `internal/controller/translator/v2/*.go` - Translators read spec to build defaults + - `internal/model/*.go` - Model layer reads spec for configuration +- ✍️ **Written by**: + - `internal/controller/wandb_v2/weightsandbiases_v2_controller.go` - Updates status fields + +#### WeightsAndBiasesSpec +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:70` +- 📖 **Read by**: + - `internal/controller/wandb_v2/weightsandbiases_v2_controller.go:111-115` - Reads Redis, Kafka, MySQL, Minio, ClickHouse, Size + - `internal/controller/translator/v2/*.go` - All translators read from this + - `internal/model/config.go` - InfraConfigBuilder uses spec fields +- ✍️ **Written by**: + - User-provided manifests + +#### WeightsAndBiasesStatus +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:225` +- 📖 **Read by**: + - `internal/controller/wandb_v2/weightsandbiases_v2_controller.go:185` - Reads RedisStatus +- ✍️ **Written by**: + - `internal/controller/wandb_v2/weightsandbiases_v2_controller.go:187` - Writes State + - `internal/controller/wandb_v2/redis.go:43` - Writes RedisStatus + - `internal/controller/wandb_v2/kafka.go:43` - Writes KafkaStatus + - `internal/controller/wandb_v2/mysql.go:43` - Writes MySQLStatus + - `internal/controller/wandb_v2/minio.go:43` - Writes MinioStatus + - `internal/controller/wandb_v2/clickhouse.go:43` - Writes ClickHouseStatus + +### MySQL Structs + +#### MySQLSpec +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:84` +- 📖 **Read by**: + - `internal/controller/translator/v2/mysql.go` - BuildMySQLSpec(), BuildMySQLDefaults() +- ✍️ **Written by**: + - `internal/controller/translator/v2/mysql.go` - Creates MySQLSpec{} literals with defaults + +#### WBMySQLConfig +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:95` +- 📖 **Read by**: + - `internal/controller/translator/v2/mysql.go` - Reads for merging with defaults +- ✍️ **Written by**: + - `internal/controller/translator/v2/mysql.go` - Creates WBMySQLConfig{} literals with defaults + +#### WBMySQLStatus +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:279` +- 📖 **Read by**: + - `internal/controller/wandb_v2/weightsandbiases_v2_controller.go` - Reads status for state aggregation +- ✍️ **Written by**: + - `internal/controller/wandb_v2/mysql.go:43` - Writes via results.ExtractMySQLStatus() + - `internal/model/mysql.go` - Creates status from MySQLStatusDetail + +#### WBMySQLConnection +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:289` +- 📖 **Read by**: + - Downstream consumers of status +- ✍️ **Written by**: + - `internal/model/mysql.go` - Creates from MySQLConnInfo + +### Redis Structs + +#### WBRedisSpec +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:99` +- 📖 **Read by**: + - `internal/controller/translator/v2/redis.go` - BuildRedisSpec(), BuildRedisDefaults() +- ✍️ **Written by**: + - `internal/controller/translator/v2/redis.go` - Creates WBRedisSpec{} literals with defaults + +#### WBRedisConfig +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:109` +- 📖 **Read by**: + - `internal/controller/translator/v2/redis.go` - Reads for merging with defaults +- ✍️ **Written by**: + - `internal/controller/translator/v2/redis.go` - Creates WBRedisConfig{} literals with defaults + +#### WBRedisSentinelSpec +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:113` +- 📖 **Read by**: + - `internal/controller/translator/v2/redis.go` - BuildRedisSpec() +- ✍️ **Written by**: + - `internal/controller/translator/v2/redis.go` - Creates WBRedisSentinelSpec{} literals with defaults + +#### WBRedisSentinelConfig +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:118` +- 📖 **Read by**: + - `internal/controller/translator/v2/redis.go` - Reads for merging with defaults +- ✍️ **Written by**: + - `internal/controller/translator/v2/redis.go` - Creates WBRedisSentinelConfig{} literals with defaults + +#### WBRedisStatus +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:305` +- 📖 **Read by**: + - `internal/controller/wandb_v2/weightsandbiases_v2_controller.go:185` - Reads for state aggregation +- ✍️ **Written by**: + - `internal/controller/wandb_v2/redis.go:43` - Writes via results.ExtractRedisStatus() + - `internal/model/redis.go` - Creates status from RedisStatusDetail + +#### WBRedisConnection +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:313` +- 📖 **Read by**: + - Downstream consumers of status +- ✍️ **Written by**: + - `internal/model/redis.go` - Creates from RedisSentinelConnInfo or RedisStandaloneConnInfo + +### Kafka Structs + +#### WBKafkaSpec +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:123` +- 📖 **Read by**: + - `internal/controller/translator/v2/kafka.go` - BuildKafkaSpec(), BuildKafkaDefaults() +- ✍️ **Written by**: + - `internal/controller/translator/v2/kafka.go` - Creates WBKafkaSpec{} literals with defaults + +#### WBKafkaConfig +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:134` +- 📖 **Read by**: + - `internal/controller/translator/v2/kafka.go` - Reads for merging with defaults +- ✍️ **Written by**: + - `internal/controller/translator/v2/kafka.go` - Creates WBKafkaConfig{} literals with defaults + +#### WBKafkaBackupSpec +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:138` +- 📖 **Read by**: + - `internal/controller/translator/v2/kafka.go` - Reads backup configuration +- ✍️ **Written by**: + - `internal/controller/translator/v2/kafka.go` - Creates WBKafkaBackupSpec{} literals with defaults + +#### WBKafkaStatus +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:326` +- 📖 **Read by**: + - `internal/controller/wandb_v2/weightsandbiases_v2_controller.go` - Reads for state aggregation +- ✍️ **Written by**: + - `internal/controller/wandb_v2/kafka.go:43` - Writes via results.ExtractKafkaStatus() + - `internal/model/kafka.go` - Creates status from KafkaStatusDetail + +#### WBKafkaConnection +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:321` +- 📖 **Read by**: + - Downstream consumers of status +- ✍️ **Written by**: + - `internal/model/kafka.go` - Creates from KafkaConnInfo + +### Minio Structs + +#### WBMinioSpec +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:146` +- 📖 **Read by**: + - `internal/controller/translator/v2/minio.go` - BuildMinioSpec(), BuildMinioDefaults() +- ✍️ **Written by**: + - `internal/controller/translator/v2/minio.go` - Creates WBMinioSpec{} literals with defaults + +#### WBMinioConfig +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:158` +- 📖 **Read by**: + - `internal/controller/translator/v2/minio.go` - Reads for merging with defaults +- ✍️ **Written by**: + - `internal/controller/translator/v2/minio.go` - Creates WBMinioConfig{} literals with defaults + +#### WBMinioBackupSpec +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:162` +- 📖 **Read by**: + - `internal/controller/translator/v2/minio.go` - Reads backup configuration +- ✍️ **Written by**: + - `internal/controller/translator/v2/minio.go` - Creates WBMinioBackupSpec{} literals with defaults + +#### WBMinioStatus +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:335` +- 📖 **Read by**: + - `internal/controller/wandb_v2/weightsandbiases_v2_controller.go` - Reads for state aggregation +- ✍️ **Written by**: + - `internal/controller/wandb_v2/minio.go:43` - Writes via results.ExtractMinioStatus() + - `internal/model/minio.go` - Creates status from MinioStatusDetail + +#### WBMinioConnection +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:345` +- 📖 **Read by**: + - Downstream consumers of status +- ✍️ **Written by**: + - `internal/model/minio.go` - Creates from MinioConnInfo + +### ClickHouse Structs + +#### WBClickHouseSpec +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:170` +- 📖 **Read by**: + - `internal/controller/translator/v2/clickhouse.go` - BuildClickHouseSpec(), BuildClickHouseDefaults() +- ✍️ **Written by**: + - `internal/controller/translator/v2/clickhouse.go` - Creates WBClickHouseSpec{} literals with defaults + +#### WBClickHouseConfig +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:183` +- 📖 **Read by**: + - `internal/controller/translator/v2/clickhouse.go` - Reads for merging with defaults +- ✍️ **Written by**: + - `internal/controller/translator/v2/clickhouse.go` - Creates WBClickHouseConfig{} literals with defaults + +#### WBClickHouseBackupSpec +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:187` +- 📖 **Read by**: + - `internal/controller/translator/v2/clickhouse.go` - Reads backup configuration +- ✍️ **Written by**: + - `internal/controller/translator/v2/clickhouse.go` - Creates WBClickHouseBackupSpec{} literals with defaults + +#### WBClickHouseStatus +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:351` +- 📖 **Read by**: + - `internal/controller/wandb_v2/weightsandbiases_v2_controller.go` - Reads for state aggregation +- ✍️ **Written by**: + - `internal/controller/wandb_v2/clickhouse.go:43` - Writes via results.ExtractClickHouseStatus() + - `internal/model/clickhouse.go` - Creates status from ClickHouseStatusDetail + +#### WBClickHouseConnection +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:361` +- 📖 **Read by**: + - Downstream consumers of status +- ✍️ **Written by**: + - `internal/model/clickhouse.go` - Creates from ClickHouseConnInfo + +### Backup Structs + +#### WBBackupSpec +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:195` +- 📖 **Read by**: + - Infrastructure model layers for backup configuration +- ✍️ **Written by**: + - User-provided manifests + +#### WBBackupS3Spec +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:211` +- 📖 **Read by**: + - Infrastructure model layers for S3 backup configuration +- ✍️ **Written by**: + - User-provided manifests + +#### WBBackupFilesystemSpec +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:218` +- 📖 **Read by**: + - Infrastructure model layers for filesystem backup configuration +- ✍️ **Written by**: + - User-provided manifests + +#### WBBackupStatus +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:295` +- 📖 **Read by**: + - Controllers for backup status monitoring +- ✍️ **Written by**: + - Infrastructure controllers during backup operations + +### Common Structs + +#### WBStatusDetail +- 📝 **Defined**: `api/v2/weightsandbiases_types.go:273` +- 📖 **Read by**: + - All status aggregation logic +- ✍️ **Written by**: + - `internal/model/*.go` - All model layers create StatusDetail instances + - Embedded in MySQL, Redis, Kafka, Minio, ClickHouse status structs + +### Application CR Types + +#### Application +- 📝 **Defined**: `api/v2/application_types.go:45` +- 📖 **Read by**: + - Application controllers (if implemented) +- ✍️ **Written by**: + - Application controllers + +#### ApplicationSpec +- 📝 **Defined**: `api/v2/application_types.go:27` +- 📖 **Read by**: + - Application controllers +- ✍️ **Written by**: + - User-provided manifests + +#### ApplicationStatus +- 📝 **Defined**: `api/v2/application_types.go:36` +- 📖 **Read by**: + - Application controllers +- ✍️ **Written by**: + - Application controllers + +--- + +## 2. Internal Model Structs (internal/model/) + +### InfraConfigBuilder +- 📝 **Defined**: `internal/model/config.go:21` +- 📖 **Read by**: + - `internal/controller/wandb_v2/weightsandbiases_v2_controller.go` - Reads built configuration + - All infrastructure reconcilers read their respective config fields +- ✍️ **Written by**: + - `internal/controller/translator/v2/*.go` - Translators populate builder with api/v2 specs that have defaults merged + +### MySQL Model Structs + +#### MySQLConfig +- 📝 **Defined**: `internal/model/mysql.go:32` +- 📖 **Read by**: + - `internal/controller/infra/mysql/percona/*.go` - Reads config to build Percona resources +- ✍️ **Written by**: + - `internal/model/mysql.go` - NewMySQLConfig() creates defaults based on Size, independent of api/v2 spec + +#### MySQLConnInfo +- 📝 **Defined**: `internal/model/mysql.go:262` +- 📖 **Read by**: + - Status reporting logic +- ✍️ **Written by**: + - `internal/model/mysql.go` - ToConnInfo() creates from MySQLConfig + +#### MySQLConnDetail +- 📝 **Defined**: `internal/model/mysql.go:268` +- 📖 **Read by**: + - Status reporting logic +- ✍️ **Written by**: + - `internal/model/mysql.go` - ToConnInfo() creates connection details + +#### MySQLStatusDetail +- 📝 **Defined**: `internal/model/mysql.go:232` +- 📖 **Read by**: + - `internal/controller/wandb_v2/mysql.go` - Reads to extract status +- ✍️ **Written by**: + - `internal/model/mysql.go` - Creates from infrastructure results + +#### MySQLSizeConfig +- 📝 **Defined**: `internal/model/mysql.go:124` +- 📖 **Read by**: + - `internal/model/mysql.go` - Reads for resource sizing +- ✍️ **Written by**: + - `internal/model/mysql.go` - Created based on Size enum (Small, Medium, Large) + +#### MySQLInfraError +- 📝 **Defined**: `internal/model/mysql.go:188` +- 📖 **Read by**: + - Error handling and status reporting +- ✍️ **Written by**: + - `internal/model/mysql.go` - Created on configuration errors + +### Redis Model Structs + +#### RedisConfig +- 📝 **Defined**: `internal/model/redis.go:20` +- 📖 **Read by**: + - `internal/controller/infra/redis/opstree/*.go` - Reads config to build Redis resources +- ✍️ **Written by**: + - `internal/model/redis.go` - NewRedisConfig() creates defaults based on Size, independent of api/v2 spec + +#### sentinelConfig +- 📝 **Defined**: `internal/model/redis.go:29` +- 📖 **Read by**: + - `internal/model/redis.go` - Internal sentinel configuration +- ✍️ **Written by**: + - `internal/model/redis.go` - Created during RedisConfig initialization + +#### RedisSentinelConnInfo +- 📝 **Defined**: `internal/model/redis.go:208` +- 📖 **Read by**: + - Status reporting for sentinel mode +- ✍️ **Written by**: + - `internal/model/redis.go` - ToConnInfo() creates for sentinel + +#### RedisSentinelConnDetail +- 📝 **Defined**: `internal/model/redis.go:223` +- 📖 **Read by**: + - Status reporting logic +- ✍️ **Written by**: + - `internal/model/redis.go` - ToConnInfo() creates connection details + +#### RedisStandaloneConnInfo +- 📝 **Defined**: `internal/model/redis.go:228` +- 📖 **Read by**: + - Status reporting for standalone mode +- ✍️ **Written by**: + - `internal/model/redis.go` - ToConnInfo() creates for standalone + +#### RedisStandaloneConnDetail +- 📝 **Defined**: `internal/model/redis.go:233` +- 📖 **Read by**: + - Status reporting logic +- ✍️ **Written by**: + - `internal/model/redis.go` - ToConnInfo() creates connection details + +#### RedisStatusDetail +- 📝 **Defined**: `internal/model/redis.go:156` +- 📖 **Read by**: + - `internal/controller/wandb_v2/redis.go` - Reads to extract status +- ✍️ **Written by**: + - `internal/model/redis.go` - Creates from infrastructure results + +#### RedisInfraError +- 📝 **Defined**: `internal/model/redis.go:98` +- 📖 **Read by**: + - Error handling and status reporting +- ✍️ **Written by**: + - `internal/model/redis.go` - Created on configuration errors + +### Kafka Model Structs + +#### KafkaConfig +- 📝 **Defined**: `internal/model/kafka.go:18` +- 📖 **Read by**: + - `internal/controller/infra/kafka/strimzi/*.go` - Reads config to build Kafka resources +- ✍️ **Written by**: + - `internal/model/kafka.go` - NewKafkaConfig() creates defaults based on Size, independent of api/v2 spec + +#### KafkaReplicationConfig +- 📝 **Defined**: `internal/model/kafka.go:27` +- 📖 **Read by**: + - `internal/model/kafka.go` - Reads for replication configuration +- ✍️ **Written by**: + - `internal/model/kafka.go` - Created during KafkaConfig initialization + +#### KafkaConnInfo +- 📝 **Defined**: `internal/model/kafka.go:196` +- 📖 **Read by**: + - Status reporting logic +- ✍️ **Written by**: + - `internal/model/kafka.go` - ToConnInfo() creates from KafkaConfig + +#### KafkaConnDetail +- 📝 **Defined**: `internal/model/kafka.go:201` +- 📖 **Read by**: + - Status reporting logic +- ✍️ **Written by**: + - `internal/model/kafka.go` - ToConnInfo() creates connection details + +#### KafkaStatusDetail +- 📝 **Defined**: `internal/model/kafka.go:188` +- 📖 **Read by**: + - `internal/controller/wandb_v2/kafka.go` - Reads to extract status +- ✍️ **Written by**: + - `internal/model/kafka.go` - Creates from infrastructure results + +#### KafkaInfraError +- 📝 **Defined**: `internal/model/kafka.go:136` +- 📖 **Read by**: + - Error handling and status reporting +- ✍️ **Written by**: + - `internal/model/kafka.go` - Created on configuration errors + +### Minio Model Structs + +#### MinioConfig +- 📝 **Defined**: `internal/model/minio.go:26` +- 📖 **Read by**: + - `internal/controller/infra/minio/tenant/*.go` - Reads config to build Minio Tenant resources +- ✍️ **Written by**: + - `internal/model/minio.go` - NewMinioConfig() creates defaults based on Size, independent of api/v2 spec + +#### MinioSizeConfig +- 📝 **Defined**: `internal/model/minio.go:88` +- 📖 **Read by**: + - `internal/model/minio.go` - Reads for resource sizing +- ✍️ **Written by**: + - `internal/model/minio.go` - Created based on Size enum (Small, Medium, Large) + +#### MinioConnInfo +- 📝 **Defined**: `internal/model/minio.go:208` +- 📖 **Read by**: + - Status reporting logic +- ✍️ **Written by**: + - `internal/model/minio.go` - ToConnInfo() creates from MinioConfig + +#### MinioConnDetail +- 📝 **Defined**: `internal/model/minio.go:214` +- 📖 **Read by**: + - Status reporting logic +- ✍️ **Written by**: + - `internal/model/minio.go` - ToConnInfo() creates connection details + +#### MinioStatusDetail +- 📝 **Defined**: `internal/model/minio.go:178` +- 📖 **Read by**: + - `internal/controller/wandb_v2/minio.go` - Reads to extract status +- ✍️ **Written by**: + - `internal/model/minio.go` - Creates from infrastructure results + +#### MinioInfraError +- 📝 **Defined**: `internal/model/minio.go:134` +- 📖 **Read by**: + - Error handling and status reporting +- ✍️ **Written by**: + - `internal/model/minio.go` - Created on configuration errors + +### ClickHouse Model Structs + +#### ClickHouseConfig +- 📝 **Defined**: `internal/model/clickhouse.go:18` +- 📖 **Read by**: + - `internal/controller/infra/clickhouse/altinity/*.go` - Reads config to build ClickHouse resources +- ✍️ **Written by**: + - `internal/model/clickhouse.go` - NewClickHouseConfig() creates defaults based on Size, independent of api/v2 spec + +#### ClickHouseConnInfo +- 📝 **Defined**: `internal/model/clickhouse.go:163` +- 📖 **Read by**: + - Status reporting logic +- ✍️ **Written by**: + - `internal/model/clickhouse.go` - ToConnInfo() creates from ClickHouseConfig + +#### ClickHouseConnDetail +- 📝 **Defined**: `internal/model/clickhouse.go:169` +- 📖 **Read by**: + - Status reporting logic +- ✍️ **Written by**: + - `internal/model/clickhouse.go` - ToConnInfo() creates connection details + +#### ClickHouseStatusDetail +- 📝 **Defined**: `internal/model/clickhouse.go:133` +- 📖 **Read by**: + - `internal/controller/wandb_v2/clickhouse.go` - Reads to extract status +- ✍️ **Written by**: + - `internal/model/clickhouse.go` - Creates from infrastructure results + +#### ClickHouseInfraError +- 📝 **Defined**: `internal/model/clickhouse.go:89` +- 📖 **Read by**: + - Error handling and status reporting +- ✍️ **Written by**: + - `internal/model/clickhouse.go` - Created on configuration errors + +### Common Model Structs + +#### InfraError +- 📝 **Defined**: `internal/model/interface.go:26` +- 📖 **Read by**: + - All infrastructure error handling +- ✍️ **Written by**: + - Implemented by MySQL, Redis, Kafka, Minio, ClickHouse InfraError types + +#### InfraStatus +- 📝 **Defined**: `internal/model/interface.go:78` +- 📖 **Read by**: + - Status aggregation logic +- ✍️ **Written by**: + - Implemented by all infrastructure StatusDetail types + +#### Results +- 📝 **Defined**: `internal/model/interface.go:132` +- 📖 **Read by**: + - `internal/controller/wandb_v2/*.go` - All infrastructure reconcilers read Results +- ✍️ **Written by**: + - Infrastructure reconciliation logic creates Results + +--- + +## 3. Vendored Operator Structs + +### Redis Operator (internal/vendored/redis-operator/) + +#### Redis +- 📝 **Defined**: `internal/vendored/redis-operator/redis/v1beta2/redis_types.go:56` +- 📖 **Read by**: + - `internal/controller/infra/redis/opstree/actual.go` - Reads actual Redis resources +- ✍️ **Written by**: + - `internal/controller/infra/redis/opstree/desired.go:37` - Creates &redisv1beta2.Redis{} + - Kubernetes API (persisted resources) + +#### RedisSpec +- 📝 **Defined**: `internal/vendored/redis-operator/redis/v1beta2/redis_types.go:29` +- 📖 **Read by**: + - `internal/controller/infra/redis/opstree/actual.go` - Reads spec fields +- ✍️ **Written by**: + - `internal/controller/infra/redis/opstree/desired.go:42` - Creates redisv1beta2.RedisSpec{} + +#### RedisStatus +- 📝 **Defined**: `internal/vendored/redis-operator/redis/v1beta2/redis_types.go:53` +- 📖 **Read by**: + - Status monitoring and reporting +- ✍️ **Written by**: + - Redis operator controller + +#### RedisSentinel +- 📝 **Defined**: `internal/vendored/redis-operator/redissentinel/v1beta2/redissentinel_types.go:46` +- 📖 **Read by**: + - `internal/controller/infra/redis/opstree/actual.go` - Reads actual RedisSentinel resources +- ✍️ **Written by**: + - `internal/controller/infra/redis/opstree/desired.go:89` - Creates redissentinelv1beta2.RedisSentinel{} + +#### RedisSentinelSpec +- 📝 **Defined**: `internal/vendored/redis-operator/redissentinel/v1beta2/redissentinel_types.go:9` +- 📖 **Read by**: + - `internal/controller/infra/redis/opstree/actual.go` - Reads spec fields +- ✍️ **Written by**: + - `internal/controller/infra/redis/opstree/desired.go:94` - Creates redissentinelv1beta2.RedisSentinelSpec{} + +#### RedisSentinelStatus +- 📝 **Defined**: `internal/vendored/redis-operator/redissentinel/v1beta2/redissentinel_types.go:43` +- 📖 **Read by**: + - Status monitoring and reporting +- ✍️ **Written by**: + - Redis operator controller + +#### RedisReplication +- 📝 **Defined**: `internal/vendored/redis-operator/redisreplication/v1beta2/redisreplication_types.go:46` +- 📖 **Read by**: + - Replication mode controllers (if used) +- ✍️ **Written by**: + - Controllers managing replication + +#### KubernetesConfig +- 📝 **Defined**: `internal/vendored/redis-operator/common/v1beta2/common_types.go:9` +- 📖 **Read by**: + - `internal/controller/infra/redis/opstree/desired.go` - Embeds in Redis specs +- ✍️ **Written by**: + - `internal/controller/infra/redis/opstree/desired.go` - Creates KubernetesConfig fields + +#### Storage +- 📝 **Defined**: `internal/vendored/redis-operator/common/v1beta2/common_types.go:148` +- 📖 **Read by**: + - Redis resource creation +- ✍️ **Written by**: + - `internal/controller/infra/redis/opstree/desired.go` - Creates Storage config + +#### RedisExporter +- 📝 **Defined**: `internal/vendored/redis-operator/common/v1beta2/common_types.go:129` +- 📖 **Read by**: + - Monitoring configuration +- ✍️ **Written by**: + - `internal/controller/infra/redis/opstree/desired.go` - Creates exporter config + +#### RedisConfig +- 📝 **Defined**: `internal/vendored/redis-operator/common/v1beta2/common_types.go:140` +- 📖 **Read by**: + - Redis configuration management +- ✍️ **Written by**: + - `internal/controller/infra/redis/opstree/desired.go` - Creates Redis config + +#### RedisSentinelConfig (common) +- 📝 **Defined**: `internal/vendored/redis-operator/common/v1beta2/common_types.go:217` +- 📖 **Read by**: + - Sentinel configuration management +- ✍️ **Written by**: + - `internal/controller/infra/redis/opstree/desired.go` - Creates sentinel config + +#### Additional Redis Common Types +- **ACLConfig**, **AdditionalVolume**, **ExistingPasswordSecret**, **InitContainer**, **Service**, **ServiceConfig**, **Sidecar**, **TLSConfig**, **RedisFollower**, **RedisLeader**, **RedisPodDisruptionBudget** +- 📖 **Read by**: Redis infrastructure controllers +- ✍️ **Written by**: `internal/controller/infra/redis/opstree/desired.go` + +### Minio Operator (internal/vendored/minio-operator/) + +#### Tenant +- 📝 **Defined**: `internal/vendored/minio-operator/minio.min.io/v2/types.go:25` +- 📖 **Read by**: + - `internal/controller/infra/minio/tenant/actual.go` - Reads actual Tenant resources +- ✍️ **Written by**: + - `internal/controller/infra/minio/tenant/desired.go:36` - Creates &miniov2.Tenant{} + - Kubernetes API (persisted resources) + +#### TenantSpec +- 📝 **Defined**: `internal/vendored/minio-operator/minio.min.io/v2/types.go:89` +- 📖 **Read by**: + - `internal/controller/infra/minio/tenant/actual.go` - Reads spec fields +- ✍️ **Written by**: + - `internal/controller/infra/minio/tenant/desired.go:44` - Creates miniov2.TenantSpec{} + +#### TenantStatus +- 📝 **Defined**: `internal/vendored/minio-operator/minio.min.io/v2/types.go:526` +- 📖 **Read by**: + - Status monitoring and reporting +- ✍️ **Written by**: + - Minio operator controller + +#### Pool +- 📝 **Defined**: `internal/vendored/minio-operator/minio.min.io/v2/types.go:640` +- 📖 **Read by**: + - Pool configuration for storage +- ✍️ **Written by**: + - `internal/controller/infra/minio/tenant/desired.go:49` - Creates []miniov2.Pool{} + +#### TenantDomains +- 📝 **Defined**: `internal/vendored/minio-operator/minio.min.io/v2/types.go:57` +- 📖 **Read by**: + - Domain configuration +- ✍️ **Written by**: + - `internal/controller/infra/minio/tenant/desired.go` - Sets domain config + +#### Features +- 📝 **Defined**: `internal/vendored/minio-operator/minio.min.io/v2/types.go:67` +- 📖 **Read by**: + - Feature flag configuration +- ✍️ **Written by**: + - `internal/controller/infra/minio/tenant/desired.go` - Sets features + +#### Additional Minio Types +- **Bucket**, **CertificateConfig**, **CertificateStatus**, **CustomCertificateConfig**, **CustomCertificates**, **ExposeServices**, **KESConfig**, **Logging**, **PoolsMetadata**, **PoolStatus**, **ServiceMetadata**, **SideCars**, **TenantScheduler**, **TenantUsage**, **TierUsage**, **AuditConfig** +- 📖 **Read by**: Minio infrastructure controllers +- ✍️ **Written by**: `internal/controller/infra/minio/tenant/desired.go` + +### Kafka Operator (internal/vendored/strimzi-kafka/) + +#### Kafka +- 📝 **Defined**: `internal/vendored/strimzi-kafka/v1beta2/kafka_types.go:255` +- 📖 **Read by**: + - `internal/controller/infra/kafka/strimzi/actual.go` - Reads actual Kafka resources +- ✍️ **Written by**: + - `internal/controller/infra/kafka/strimzi/desired.go:26` - Creates &v1beta3.Kafka{} + - Kubernetes API (persisted resources) + +#### KafkaSpec +- 📝 **Defined**: `internal/vendored/strimzi-kafka/v1beta2/kafka_types.go:25` +- 📖 **Read by**: + - `internal/controller/infra/kafka/strimzi/actual.go` - Reads spec fields +- ✍️ **Written by**: + - `internal/controller/infra/kafka/strimzi/desired.go:37` - Creates v1beta3.KafkaSpec{} + +#### KafkaStatus +- 📝 **Defined**: `internal/vendored/strimzi-kafka/v1beta2/kafka_types.go:229` +- 📖 **Read by**: + - Status monitoring and reporting +- ✍️ **Written by**: + - Strimzi Kafka operator + +#### KafkaClusterSpec +- 📝 **Defined**: `internal/vendored/strimzi-kafka/v1beta2/kafka_types.go:32` +- 📖 **Read by**: + - Cluster configuration +- ✍️ **Written by**: + - `internal/controller/infra/kafka/strimzi/desired.go:38` - Creates v1beta3.KafkaClusterSpec{} + +#### KafkaNodePool +- 📝 **Defined**: `internal/vendored/strimzi-kafka/v1beta2/kafkanodepool_types.go:76` +- 📖 **Read by**: + - Node pool configuration +- ✍️ **Written by**: + - `internal/controller/infra/kafka/strimzi/desired.go:93` - Creates &v1beta3.KafkaNodePool{} + +#### KafkaNodePoolSpec +- 📝 **Defined**: `internal/vendored/strimzi-kafka/v1beta2/kafkanodepool_types.go:25` +- 📖 **Read by**: + - Node pool spec configuration +- ✍️ **Written by**: + - `internal/controller/infra/kafka/strimzi/desired.go` - Creates node pool specs + +#### GenericKafkaListener +- 📝 **Defined**: `internal/vendored/strimzi-kafka/v1beta2/kafka_types.go:45` +- 📖 **Read by**: + - Listener configuration +- ✍️ **Written by**: + - `internal/controller/infra/kafka/strimzi/desired.go` - Creates listeners + +#### Additional Kafka Types +- **EntityOperatorSpec**, **EntityTopicOperatorSpec**, **EntityUserOperatorSpec**, **GenericKafkaListenerConfiguration**, **KafkaListenerAuthentication**, **KafkaListenerConfigurationBootstrap**, **KafkaListenerConfigurationBroker**, **KafkaStorage**, **KRaftMetadataStorage**, **StorageVolume**, **ZooKeeperSpec**, **ContainerTemplate**, **EntityOperatorLogging**, **EntityOperatorTemplate**, **KafkaClusterTemplate**, **MetadataTemplate**, **PodTemplate**, **ResourceTemplate**, **StatefulSetTemplate**, **Rack**, **ListenerAddress**, **ListenerStatus**, **PodSetTemplate**, **JvmOptions**, **SystemProperty**, **ZooKeeperClusterTemplate** +- 📖 **Read by**: Kafka infrastructure controllers +- ✍️ **Written by**: `internal/controller/infra/kafka/strimzi/desired.go` + +### MySQL Operator (internal/vendored/percona-operator/) + +#### PerconaXtraDBCluster +- 📝 **Defined**: `internal/vendored/percona-operator/pxc/v1/pxc_types.go:323` +- 📖 **Read by**: + - `internal/controller/infra/mysql/percona/actual.go` - Reads actual PXC resources +- ✍️ **Written by**: + - `internal/controller/infra/mysql/percona/desired.go:36` - Creates &pxcv1.PerconaXtraDBCluster{} + - Kubernetes API (persisted resources) + +#### PerconaXtraDBClusterSpec +- 📝 **Defined**: `internal/vendored/percona-operator/pxc/v1/pxc_types.go:30` +- 📖 **Read by**: + - `internal/controller/infra/mysql/percona/actual.go` - Reads spec fields +- ✍️ **Written by**: + - `internal/controller/infra/mysql/percona/desired.go:44` - Creates pxcv1.PerconaXtraDBClusterSpec{} + +#### PerconaXtraDBClusterStatus +- 📝 **Defined**: `internal/vendored/percona-operator/pxc/v1/pxc_types.go:266` +- 📖 **Read by**: + - Status monitoring and reporting +- ✍️ **Written by**: + - Percona operator controller + +#### PXCSpec +- 📝 **Defined**: `internal/vendored/percona-operator/pxc/v1/pxc_types.go:90` +- 📖 **Read by**: + - PXC node configuration +- ✍️ **Written by**: + - `internal/controller/infra/mysql/percona/desired.go:51` - Creates &pxcv1.PXCSpec{} + +#### ProxySQLSpec +- 📝 **Defined**: `internal/vendored/percona-operator/pxc/v1/pxc_types.go:574` +- 📖 **Read by**: + - ProxySQL configuration +- ✍️ **Written by**: + - `internal/controller/infra/mysql/percona/desired.go:82` - Creates &pxcv1.ProxySQLSpec{} + +#### HAProxySpec +- 📝 **Defined**: `internal/vendored/percona-operator/pxc/v1/pxc_types.go:579` +- 📖 **Read by**: + - HAProxy configuration +- ✍️ **Written by**: + - `internal/controller/infra/mysql/percona/desired.go` - Creates HAProxy config + +#### BackupStorageSpec +- 📝 **Defined**: `internal/vendored/percona-operator/pxc/v1/pxc_types.go:657` +- 📖 **Read by**: + - Backup configuration +- ✍️ **Written by**: + - `internal/controller/infra/mysql/percona/desired.go` - Creates backup storage config + +#### Additional MySQL Types +- **AppStatus**, **BackupContainerArgs**, **BackupContainerOptions**, **BackupStorageAzureSpec**, **BackupStorageS3Spec**, **ClusterCondition**, **ComponentStatus**, **InitContainerSpec**, **LogCollectorSpec**, **PerconaXtraDBClusterBackup**, **PerconaXtraDBClusterRestore**, **PITR**, **PITRSpec**, **PMMSpec**, **PodAffinity**, **PodDisruptionBudgetSpec**, **PodSpec**, **PXCScheduledBackup**, **ReplicasServiceExpose**, **ReplicationChannel**, **ReplicationChannelConfig**, **ReplicationChannelStatus**, **ReplicationSource**, **ReplicationStatus**, **SecretKeySelector**, **ServiceExpose**, **TLSSpec**, **UnsafeFlags**, **UpgradeOptions**, **User**, **Volume**, **VolumeSpec**, **MySQLConfig**, **MySQLSizeConfig** +- 📖 **Read by**: MySQL infrastructure controllers +- ✍️ **Written by**: `internal/controller/infra/mysql/percona/desired.go` + +### ClickHouse Operator (internal/vendored/altinity-clickhouse/) + +#### ClickHouseInstallation +- 📝 **Defined**: `internal/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/types.go:36` +- 📖 **Read by**: + - `internal/controller/infra/clickhouse/altinity/actual.go` - Reads actual CHI resources +- ✍️ **Written by**: + - `internal/controller/infra/clickhouse/altinity/desired.go:49` - Creates &v2.ClickHouseInstallation{} + - Kubernetes API (persisted resources) + +#### ChiSpec +- 📝 **Defined**: `internal/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_spec.go:22` +- 📖 **Read by**: + - `internal/controller/infra/clickhouse/altinity/actual.go` - Reads spec fields +- ✍️ **Written by**: + - `internal/controller/infra/clickhouse/altinity/desired.go:57` - Creates v2.ChiSpec{} + +#### Status +- 📝 **Defined**: `internal/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_status.go:46` +- 📖 **Read by**: + - Status monitoring and reporting +- ✍️ **Written by**: + - ClickHouse operator controller + +#### Configuration +- 📝 **Defined**: `internal/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_configuration_chi.go:46` +- 📖 **Read by**: + - Configuration management +- ✍️ **Written by**: + - `internal/controller/infra/clickhouse/altinity/desired.go:58` - Creates &v2.Configuration{} + +#### Cluster +- 📝 **Defined**: `internal/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_cluster.go:22` +- 📖 **Read by**: + - Cluster configuration +- ✍️ **Written by**: + - `internal/controller/infra/clickhouse/altinity/desired.go` - Creates cluster config + +#### Templates +- 📝 **Defined**: `internal/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_templates.go:25` +- 📖 **Read by**: + - Template configuration +- ✍️ **Written by**: + - `internal/controller/infra/clickhouse/altinity/desired.go:76` - Creates &v2.Templates{} + +#### Additional ClickHouse Types (extensive list) +- **ActionPlan**, **AddonConfiguration**, **AddonSpec**, **ChiClusterAddress**, **ChiClusterLayout**, **ChiClusterRuntime**, **ChiShardAddress**, **ChiShardRuntime**, **ChiShard**, **ClickHouseInstallationRuntime**, **ClickHouseOperatorConfiguration**, **Cleanup**, **ClusterSecret**, **ConfigCRSource**, **Defaults**, **FillStatusParams**, **Host**, **HostPorts**, **HostRuntime**, **HostSecure**, **HostSettings**, **MacrosSection**, **MacrosSections**, **ObjectsCleanup**, **OperatorConfig**, **OperatorConfigAddons**, **OperatorConfigAddonRule**, **OperatorConfigAnnotation**, **OperatorConfigCHI**, **OperatorConfigCHIRuntime**, **OperatorConfigClickHouse**, **OperatorConfigConfig**, **OperatorConfigDefault**, **OperatorConfigFile**, **OperatorConfigFileRuntime**, **OperatorConfigKeeper**, **OperatorConfigLabel**, **OperatorConfigLabelRuntime**, **OperatorConfigMetrics**, **OperatorConfigMetricsLabels**, **OperatorConfigReconcile**, **OperatorConfigReconcileRuntime**, **OperatorConfigRestartPolicy**, **OperatorConfigRestartPolicyRule**, **OperatorConfigRuntime**, **OperatorConfigStatus**, **OperatorConfigStatusFields**, **OperatorConfigTemplate**, **OperatorConfigUser**, **OperatorConfigWatch**, **OperatorConfigWatchNamespaces**, **PodDistribution**, **PodTemplateZone**, **ReconcileHost**, **ReconcileHostDrop**, **ReconcileHostDropReplicas**, **ReconcileHostWait**, **ReconcileHostWaitProbes**, **ReconcileHostWaitReplicas**, **ReconcileMacros**, **SchemaPolicy**, **Setting**, **SettingSource**, **Settings**, **SettingsNormalizerOptions**, **TemplatesList**, **VolumeClaimTemplate**, **ZookeeperConfig**, **ZookeeperNode** +- 📖 **Read by**: ClickHouse infrastructure controllers +- ✍️ **Written by**: `internal/controller/infra/clickhouse/altinity/desired.go` + +--- + +## 4. Usage Summary by Package + +### internal/controller/wandb_v2/ +**Primary Role**: Orchestrates infrastructure reconciliation and status updates + +**Reads from**: +- `api/v2.WeightsAndBiases` - Main CR resource +- `api/v2.WeightsAndBiasesSpec` - Reads Redis, Kafka, MySQL, Minio, ClickHouse, Size specs +- `api/v2.*Status` - Reads all infrastructure status fields + +**Writes to**: +- `api/v2.WeightsAndBiasesStatus` - Updates State field +- `api/v2.WBRedisStatus`, `api/v2.WBKafkaStatus`, `api/v2.WBMySQLStatus`, `api/v2.WBMinioStatus`, `api/v2.WBClickHouseStatus` - Updates all infrastructure status + +**Files**: +- `weightsandbiases_v2_controller.go` - Main orchestration +- `redis.go`, `kafka.go`, `mysql.go`, `minio.go`, `clickhouse.go` - Infrastructure-specific reconcilers + +### internal/controller/translator/v2/ +**Primary Role**: Builds defaults and merges user specs with defaults + +**Reads from**: +- `api/v2.WB*Spec` types - All infrastructure spec types +- `api/v2.WB*Config` types - All infrastructure config types + +**Writes to**: +- `api/v2.WB*Spec` types - Creates struct literals with defaults +- `api/v2.WB*Config` types - Creates struct literals with defaults + +**Files**: +- `redis.go`, `kafka.go`, `mysql.go`, `minio.go`, `clickhouse.go` - Infrastructure-specific translators + +### internal/model/ +**Primary Role**: Business logic layer for configuration and status + +**Reads from**: +- `internal/model.*Config` types - Reads its own config structs for transformations +- `internal/model.*StatusDetail` types - Reads its own status structs for conversions +- `internal/model.*ConnInfo` types - Reads its own connection info structs for conversions + +**Writes to**: +- `internal/model.*Config` types - Creates internal config structs based on Size defaults +- `internal/model.*StatusDetail` types - Creates status detail structs from infra results +- `internal/model.*ConnInfo` types - Creates connection info structs from configs +- `api/v2.WB*Status` types - Converts StatusDetail to API status structs +- `api/v2.WB*Connection` types - Converts ConnInfo to API connection structs + +**Files**: +- `config.go` - InfraConfigBuilder (holds api/v2 specs populated by translator) +- `redis.go`, `kafka.go`, `mysql.go`, `minio.go`, `clickhouse.go` - Infrastructure-specific models +- `interface.go` - Common interfaces + +**Key Note**: internal/model does NOT import or read from api/v2 types. It creates default configs based on Size enums and converts its own internal types to api/v2 status types. + +### internal/controller/infra/*/ +**Primary Role**: Creates actual Kubernetes resources for infrastructure operators + +#### internal/controller/infra/redis/opstree/ +**Reads from**: +- `internal/model.RedisConfig` - Reads Redis configuration +- `internal/vendored/redis-operator/v1beta2.Redis` - Reads actual resources +- `internal/vendored/redis-operator/v1beta2.RedisSentinel` - Reads actual sentinel resources + +**Writes to**: +- `internal/vendored/redis-operator/v1beta2.Redis` - Creates Redis{} instances +- `internal/vendored/redis-operator/v1beta2.RedisSpec` - Creates RedisSpec{} instances +- `internal/vendored/redis-operator/v1beta2.RedisSentinel` - Creates RedisSentinel{} instances +- `internal/vendored/redis-operator/v1beta2.RedisSentinelSpec` - Creates RedisSentinelSpec{} instances +- `internal/vendored/redis-operator/common/v1beta2.*` - Creates all common types + +**Files**: +- `desired.go` - Creates desired resources +- `actual.go` - Reads actual resources + +#### internal/controller/infra/kafka/strimzi/ +**Reads from**: +- `internal/model.KafkaConfig` - Reads Kafka configuration +- `internal/vendored/strimzi-kafka/v1beta2.Kafka` - Reads actual resources + +**Writes to**: +- `internal/vendored/strimzi-kafka/v1beta2.Kafka` - Creates Kafka{} instances +- `internal/vendored/strimzi-kafka/v1beta2.KafkaSpec` - Creates KafkaSpec{} instances +- `internal/vendored/strimzi-kafka/v1beta2.KafkaClusterSpec` - Creates KafkaClusterSpec{} instances +- `internal/vendored/strimzi-kafka/v1beta2.KafkaNodePool` - Creates KafkaNodePool{} instances +- All Kafka template and configuration types + +**Files**: +- `desired.go` - Creates desired resources +- `actual.go` - Reads actual resources + +#### internal/controller/infra/mysql/percona/ +**Reads from**: +- `internal/model.MySQLConfig` - Reads MySQL configuration +- `internal/vendored/percona-operator/pxc/v1.PerconaXtraDBCluster` - Reads actual resources + +**Writes to**: +- `internal/vendored/percona-operator/pxc/v1.PerconaXtraDBCluster` - Creates PerconaXtraDBCluster{} instances +- `internal/vendored/percona-operator/pxc/v1.PerconaXtraDBClusterSpec` - Creates spec instances +- `internal/vendored/percona-operator/pxc/v1.PXCSpec` - Creates PXC node spec instances +- `internal/vendored/percona-operator/pxc/v1.ProxySQLSpec` - Creates ProxySQL spec instances +- All PXC configuration types + +**Files**: +- `desired.go` - Creates desired resources +- `actual.go` - Reads actual resources + +#### internal/controller/infra/minio/tenant/ +**Reads from**: +- `internal/model.MinioConfig` - Reads Minio configuration +- `internal/vendored/minio-operator/minio.min.io/v2.Tenant` - Reads actual resources + +**Writes to**: +- `internal/vendored/minio-operator/minio.min.io/v2.Tenant` - Creates Tenant{} instances +- `internal/vendored/minio-operator/minio.min.io/v2.TenantSpec` - Creates TenantSpec{} instances +- `internal/vendored/minio-operator/minio.min.io/v2.Pool` - Creates Pool{} instances +- All Minio tenant configuration types + +**Files**: +- `desired.go` - Creates desired resources +- `actual.go` - Reads actual resources + +#### internal/controller/infra/clickhouse/altinity/ +**Reads from**: +- `internal/model.ClickHouseConfig` - Reads ClickHouse configuration +- `internal/vendored/altinity-clickhouse/clickhouse.altinity.com/v1.ClickHouseInstallation` - Reads actual resources + +**Writes to**: +- `internal/vendored/altinity-clickhouse/clickhouse.altinity.com/v1.ClickHouseInstallation` - Creates CHI instances +- `internal/vendored/altinity-clickhouse/clickhouse.altinity.com/v1.ChiSpec` - Creates ChiSpec{} instances +- `internal/vendored/altinity-clickhouse/clickhouse.altinity.com/v1.Configuration` - Creates Configuration{} instances +- `internal/vendored/altinity-clickhouse/clickhouse.altinity.com/v1.Templates` - Creates Templates{} instances +- All ClickHouse configuration types + +**Files**: +- `desired.go` - Creates desired resources +- `actual.go` - Reads actual resources + +--- + +## 5. Data Flow Diagram + +``` +User Manifest (api/v2.WeightsAndBiasesSpec) + ↓ +internal/controller/wandb_v2/weightsandbiases_v2_controller.go + ↓ (reads WeightsAndBiasesSpec) + ↓ +internal/controller/translator/v2/*.go (TRANSLATOR LAYER - THE BRIDGE) + ↓ (reads api/v2.WB*Spec from user) + ↓ (builds defaults for missing fields) + ↓ (merges user input with defaults) + ↓ (writes back to api/v2.WB*Spec with complete defaults) + ↓ (populates InfraConfigBuilder with merged specs) + ↓ +internal/model/config.go (InfraConfigBuilder) + ↓ (holds api/v2 specs with defaults - NO direct api/v2 import in model) + ↓ (passed to model constructors as plain structs) + ↓ +internal/model/*.go (RedisConfig, KafkaConfig, MySQLConfig, MinioConfig, ClickHouseConfig) + ↓ (creates internal config structs using Size-based defaults) + ↓ (NO direct reading of api/v2 types) + ↓ +internal/controller/infra/*/desired.go + ↓ (reads internal/model.*Config) + ↓ (creates vendored operator structs) + ↓ +Vendored Operator CRs (Redis, Kafka, MySQL, Minio, ClickHouse) + ↓ +Kubernetes API + ↓ +Operator Controllers (external) + ↓ (updates vendored operator status) + ↓ +internal/controller/infra/*/actual.go + ↓ (reads vendored operator status) + ↓ (creates internal/model.*StatusDetail) + ↓ +internal/model/*.go + ↓ (converts internal StatusDetail → api/v2.WB*Status) + ↓ (converts internal ConnInfo → api/v2.WB*Connection) + ↓ +internal/controller/wandb_v2/*.go + ↓ (writes status back to api/v2) + ↓ +api/v2.WeightsAndBiasesStatus +``` + +**Key Points**: +- **Translator is the bridge** between api/v2 and model +- **internal/model does NOT import api/v2** - it creates Size-based defaults +- **Data flows**: api/v2 → translator → model → infra → vendored operators +- **Status flows**: vendored operators → infra → model → translator → api/v2 + +--- + +## 6. Cross-Package Struct Dependencies + +### internal/controller/translator/v2 ↔ api/v2 (THE BRIDGE) +- **Translator reads**: All `WB*Spec` and `WB*Config` types from user input +- **Translator writes**: All `WB*Spec` and `WB*Config` types with defaults merged +- **Role**: The ONLY layer that directly manipulates api/v2 infrastructure spec types + +### internal/controller/translator/v2 → internal/model +- **Translator writes**: Populates `InfraConfigBuilder` with api/v2 specs that have defaults +- **Model reads**: Accesses specs from builder (NOT direct api/v2 import) + +### internal/model → internal/model (internal conversions) +- `*Config` types → `*ConnInfo` types (via ToConnInfo() methods) +- `*StatusDetail` types → `api/v2.WB*Status` types (via ToStatus() methods) +- `*ConnInfo` types → `api/v2.WB*Connection` types (via conversion methods) + +### internal/model → internal/controller/infra/* +- All `*Config` types (RedisConfig, KafkaConfig, MySQLConfig, MinioConfig, ClickHouseConfig) are read by infrastructure controllers + +### internal/controller/infra/* ↔ vendored operators +- **Infra writes**: Creates all vendored operator struct types (Redis, Kafka, MySQL, Minio, ClickHouse CRs) +- **Infra reads**: Reads actual vendored operator resources and status + +### internal/controller/infra/* → internal/model +- **Infra writes**: Creates `*StatusDetail` types from vendored operator status + +### internal/controller/wandb_v2 → api/v2 +- **Main controller reads**: All spec types (WeightsAndBiasesSpec) +- **Main controller writes**: All status types (WeightsAndBiasesStatus) + +### Key Architectural Principle +**internal/model does NOT import api/v2**. The translator layer acts as the bridge: +- User input (api/v2) → Translator → InfraConfigBuilder → Model +- Model status → Translator (optional) → api/v2 status +- This separation keeps the model layer decoupled from the API layer + +--- + +## 7. Method Receivers Summary + +### api/v2 Types +All API types have generated DeepCopy methods (method receivers): +- `func (in *WeightsAndBiases) DeepCopy() *WeightsAndBiases` +- `func (in *WeightsAndBiasesSpec) DeepCopy() *WeightsAndBiasesSpec` +- Similar for all API struct types + +### internal/model Types +Model types have business logic method receivers: +- `func (c *RedisConfig) ToConnInfo() RedisConnInfo` +- `func (c *MySQLConfig) ToConnInfo() MySQLConnInfo` +- `func (c *KafkaConfig) ToConnInfo() KafkaConnInfo` +- `func (c *MinioConfig) ToConnInfo() MinioConnInfo` +- `func (c *ClickHouseConfig) ToConnInfo() ClickHouseConnInfo` +- `func (s *RedisStatusDetail) ToStatus() WBRedisStatus` +- Similar patterns for all infrastructure types + +### Vendored Operator Types +Vendored operator types have their own generated DeepCopy methods and operator-specific methods. + +--- + +## Notes + +1. **api/v2** defines the user-facing API (CRD types) +2. **internal/controller/translator/v2** is THE BRIDGE - reads api/v2, builds defaults, merges, writes back to api/v2 +3. **internal/model** does NOT import api/v2 - creates Size-based defaults and converts internal types to api/v2 status +4. **internal/controller/infra** creates actual operator CRs from internal/model configs +5. **internal/controller/wandb_v2** orchestrates the entire flow and updates status + +All struct usage follows a clear pattern: +- **Config Flow**: User spec (api/v2) → Translator (bridge) → InfraConfigBuilder → Model (no api/v2 import) → Infrastructure controller → Vendored operators +- **Status Flow**: Vendored operators → Infrastructure controller → Model StatusDetail → Model conversion → api/v2 Status + +**Critical Architectural Point**: internal/model is decoupled from api/v2. The translator acts as the adapter/bridge layer. diff --git a/docs/design/wandb_v2/tilt.md b/docs/design/wandb_v2/tilt.md new file mode 100644 index 00000000..ddf0936a --- /dev/null +++ b/docs/design/wandb_v2/tilt.md @@ -0,0 +1,90 @@ +# Tilt Resource Dependency Graph + +Resources shown with their labels in parentheses. The default path installs one +`wandb-operator` Helm release, Gateway API networking, an optional W&B CR, and +telemetry disabled. + +Conditional resources: + +- `Wandb`, `Wandb-Endpoint`, and `WandB-CA` only appear when `includeCR=True`. +- `WandB-Namespace` appears when Tilt needs a W&B namespace for the CR or telemetry resources. +- `gateway-api-crds` and `nginx-gateway-fabric` appear when `networkMode="gateway"`. +- `ingress-nginx-*` appears when `networkMode="ingress"`. +- `Telemetry-Endpoint-*` appears only when `observabilityMode="full"`. + +Tilt generates the W&B CR through `go run ./hack/tilt/wandbcr`, then reads the +typed YAML back for the resource name, namespace, networking mode, and endpoint +hostname. The default CR omits `spec.wandb.manifestRepository` so the webhook +uses the published server manifest repository; `manifestSource="local"` mounts +`localManifestPath` into the operator image at `/server-manifest` and writes +`file:///server-manifest` into the generated CR. + +```mermaid +graph TD + %% Bootstrap and dependencies + operator_codegen["Operator-Codegen\n(Wandb-Operator)"] + operator_build["Operator-Build\n(Wandb-Operator)"] + operator_chart_deps["Operator-Chart-Deps\n(Dependencies)"] + wandb_crds_apply["WandB-CRDs-Apply\n(Dependencies)"] + wandb_crds_ready["WandB-CRDs-Ready\n(Dependencies)"] + cert_manager["cert-manager\n(Dependencies)"] + gateway_api_crds["gateway-api-crds\n(Dependencies)"] + nginx_gateway_fabric["nginx-gateway-fabric\n(Dependencies)"] + ingress_nginx_repo["ingress-nginx-repo\n(Dependencies)"] + ingress_nginx_controller["ingress-nginx-controller\n(Dependencies)"] + wandb_namespace["WandB-Namespace\n(Dependencies)"] + + operator_codegen --> operator_build + operator_codegen --> wandb_crds_apply + wandb_crds_apply --> wandb_crds_ready + gateway_api_crds --> nginx_gateway_fabric + ingress_nginx_repo --> ingress_nginx_controller + + %% Operator install + wandb_operator["wandb-operator\n(Wandb-Operator)"] + operator_chart_deps --> wandb_operator + operator_build --> wandb_operator + wandb_crds_ready --> wandb_operator + cert_manager --> wandb_operator + nginx_gateway_fabric --> wandb_operator + wandb_namespace --> wandb_operator + + %% Webhook and W&B CR + operator_webhook_ready["Operator-Webhook-Ready\n(Wandb-Operator)"] + wandb_ca["WandB-CA\n(Wandb-App)"] + wandb["Wandb\n(Wandb-App)"] + wandb_endpoint["Wandb-Endpoint\n(Wandb-App)"] + dev_clean["Dev-Clean\n(Wandb-App)"] + + wandb_operator --> operator_webhook_ready + cert_manager --> wandb_ca + operator_webhook_ready --> wandb + wandb_namespace --> wandb + wandb_ca --> wandb + nginx_gateway_fabric --> wandb + ingress_nginx_controller --> wandb + wandb --> wandb_endpoint + nginx_gateway_fabric --> wandb_endpoint + ingress_nginx_controller --> wandb_endpoint + + %% Telemetry endpoint port-forwards + telemetry_grafana["Telemetry-Endpoint-Grafana\n(Telemetry)"] + telemetry_metrics["Telemetry-Endpoint-VictoriaMetrics\n(Telemetry)"] + telemetry_logs["Telemetry-Endpoint-VictoriaLogs\n(Telemetry)"] + telemetry_traces["Telemetry-Endpoint-VictoriaTraces\n(Telemetry)"] + + wandb_operator --> telemetry_grafana + wandb_operator --> telemetry_metrics + wandb_operator --> telemetry_logs + wandb_operator --> telemetry_traces + + classDef dependencies fill:#f5f5f5,stroke:#777 + classDef operator fill:#dbeafe,stroke:#2563eb + classDef wandb fill:#ede9fe,stroke:#7c3aed + classDef telemetry fill:#fce7f3,stroke:#db2777 + + class operator_chart_deps,wandb_crds_apply,wandb_crds_ready,cert_manager,gateway_api_crds,nginx_gateway_fabric,ingress_nginx_repo,ingress_nginx_controller,wandb_namespace dependencies + class operator_codegen,operator_build,wandb_operator,operator_webhook_ready operator + class wandb_ca,wandb,wandb_endpoint,dev_clean wandb + class telemetry_grafana,telemetry_metrics,telemetry_logs,telemetry_traces telemetry +``` diff --git a/docs/infra-connection-settings.md b/docs/infra-connection-settings.md new file mode 100644 index 00000000..cb5a5435 --- /dev/null +++ b/docs/infra-connection-settings.md @@ -0,0 +1,110 @@ +# Infrastructure Connection Settings + +All Go URL spec components are parsed by the shared `connectors.ParseConnectionString()` into `connectors.ConnectionInfo`. +Query params are parsed per-connector via `queryparams.ParseAndValidate()` into typed structs (e.g., `RedisQueryParams`). +Weave-python env vars are defined in `services/weave-python/weave-public/weave/trace_server/environment.py`. + +| Infra | Component | Setting | Type | Default | Notes | +|-------|-----------|---------|------|---------|-------| +| **Redis** | `connectors.ConnectionInfo`<br>`weave-python env` | `scheme`<br>`WEAVE_REDIS_URL.Scheme` | string | `redis` | Only `redis://` supported; TLS via query param | +| Redis | `connectors.ConnectionInfo`<br>`weave-python env` | `host`<br>`WEAVE_REDIS_URL.Host` | string | — | Required | +| Redis | `connectors.ConnectionInfo`<br>`weave-python env` | `port`<br>`WEAVE_REDIS_URL.Port` | int | — | Typically 6379; 26379 for Sentinel | +| Redis | `connectors.ConnectionInfo`<br>`weave-python env` | `username`<br>`WEAVE_REDIS_URL.Username` | string | — | Extracted but typically ignored | +| Redis | `connectors.ConnectionInfo`<br>`weave-python env` | `password`<br>`WEAVE_REDIS_URL.Password` | string | — | From userinfo | +| Redis | `connectors.RedisQueryParams` | `tls` | bool | false | Enables TLS (min TLS 1.2) | +| Redis | `connectors.RedisQueryParams` | `caCertPath` | string | — | Path to CA certificate file | +| Redis | `connectors.RedisQueryParams` | `ttlInSeconds` | int64 | 0 | Cache TTL | +| Redis | `connectors.RedisQueryParams` | `master` | string | — | Sentinel master name; enables failover mode | +| Redis | `connectors.RedisQueryParams` | `cluster` | bool | false | Enables Redis Cluster mode | +| Redis | `connectors.RedisQueryParams` | `poolSize` | int | 0 (auto) | Connection pool size | +| Redis | `connectors.RedisQueryParams` | `poolSizeMultiple` | int | — | Multiple of GOMAXPROCS | +| Redis | `connectors.RedisQueryParams` | `minIdleConns` | int | 0 | Minimum idle connections | +| Redis | `connectors.RedisQueryParams` | `dialTimeout` | duration | — | Dial timeout | +| Redis | `connectors.RedisQueryParams` | `readTimeout` | duration | — | Read timeout | +| Redis | `connectors.RedisQueryParams` | `writeTimeout` | duration | — | Write timeout | +| Redis | `connectors.RedisQueryParams` | `contextTimeoutEnabled` | bool | — | Enable context-based timeouts | +| Redis | `connectors.RedisQueryParams` | `poolTimeout` | duration | — | Pool wait timeout | +| Redis | `connectors.RedisQueryParams` | `connMaxIdleTime` | duration | — | Max idle connection lifetime | +| Redis | `connectors.RedisQueryParams` | `connMaxLifetime` | duration | — | Max connection lifetime | +| Redis | `connectors.RedisQueryParams` | `connMaxLifetimeJitter` | duration | — | Jitter for connection lifetime | +| Redis | `connectors.RedisQueryParams` | `maxRetries` | int | — | Max retry attempts | +| Redis | `connectors.RedisQueryParams` | `dialerRetries` | int | — | Dialer-specific retries | +| Redis | `connectors.RedisQueryParams` | `maxConcurrentDials` | int | — | Concurrent dial limit | +| Redis | `connectors.RedisQueryParams` | `dialerRetryTimeout` | duration | — | Timeout for dialer retries | +| Redis | `connectors.RedisQueryParams` | `minRetryBackoff` | duration | — | Min exponential backoff | +| Redis | `connectors.RedisQueryParams` | `maxRetryBackoff` | duration | — | Max exponential backoff | +| Redis | `connectors.RedisQueryParams` | `minDialerRetryBackoff` | duration | — | Min dialer backoff | +| Redis | `connectors.RedisQueryParams` | `maxDialerRetryBackoff` | duration | — | Max dialer backoff | +| **MySQL** | `connectors.ConnectionInfo` | `scheme` | string | `mysql` | Also: `mysql-replica`, `cloudsql`, `cloudsql-replica` | +| MySQL | `connectors.ConnectionInfo` | `host` | string | — | Required | +| MySQL | `connectors.ConnectionInfo` | `port` | int | 3306 | — | +| MySQL | `connectors.ConnectionInfo` | `username` | string | — | DB user | +| MySQL | `connectors.ConnectionInfo` | `password` | string | — | DB password | +| MySQL | `connectors.ConnectionInfo` | `path (database)` | string | — | Database name | +| MySQL | `connectors.MySQLQueryParams` | `tls` | string | — | `true`, `false`, `skip-verify`, `preferred`, `custom` | +| MySQL | `connectors.MySQLQueryParams` | `ssl-ca` | string | — | CA cert path (required for tls=custom) | +| MySQL | `connectors.MySQLQueryParams` | `ssl-cert` | string | — | Client cert path (optional for tls=custom) | +| MySQL | `connectors.MySQLQueryParams` | `ssl-key` | string | — | Client key path (optional for tls=custom) | +| **ClickHouse** | `connectors.ConnectionInfo` | `scheme` | string | `clickhouse` | Also: `clickhouse-sql` | +| ClickHouse | `connectors.ConnectionInfo`<br>`weave-python env` | `host`<br>`WF_CLICKHOUSE_HOST` | string | `localhost` | Required | +| ClickHouse | `connectors.ConnectionInfo`<br>`weave-python env` | `port`<br>`WF_CLICKHOUSE_PORT` | int | 9000 / 8123 | Go: 9000 native, 8123 HTTP; Python default: 8123 | +| ClickHouse | `connectors.ConnectionInfo`<br>`weave-python env` | `username`<br>`WF_CLICKHOUSE_USER` | string | `default` | DB user | +| ClickHouse | `connectors.ConnectionInfo`<br>`weave-python env` | `password`<br>`WF_CLICKHOUSE_PASS` | string | `""` | DB password | +| ClickHouse | `connectors.ConnectionInfo`<br>`weave-python env` | `path (database)`<br>`WF_CLICKHOUSE_DATABASE` | string | `default` | Database name | +| ClickHouse | `connectors.ClickhouseQueryParams` | `tls` | bool | true | Enables TLS (min TLS 1.3); Python auto-detects via port 8443 | +| ClickHouse | `connectors.ClickhouseQueryParams` | `max_idle_conns` | int | 5 | Max idle connections | +| ClickHouse | `connectors.ClickhouseQueryParams` | `max_open_conns` | int | idle+5 | Max open connections | +| ClickHouse | `connectors.ClickhouseQueryParams` | `conn_max_lifetime` | duration | 10m | Max connection lifetime | +| ClickHouse | `connectors.ClickhouseQueryParams` | `dial_timeout` | duration | 30s | Connection timeout | +| ClickHouse | `connectors.ClickhouseQueryParams` | `client_name` | string | `megabinary` | Client identifier | +| ClickHouse | `connectors.ClickhouseQueryParams` | `protocol` | string | `native` | `native` or `http` | +| ClickHouse | `connectors.ClickhouseQueryParams` | `fail-fast` | bool | false | Fail fast on connection errors | +| ClickHouse | `connectors.ClickhouseQueryParams` | `auto-create` | bool | true | Auto-create database if missing | +| ClickHouse | `weave-python env` | `WF_CLICKHOUSE_REPLICATED` | string | `false` | Enable replication | +| ClickHouse | `weave-python env` | `WF_CLICKHOUSE_REPLICATED_PATH` | string | None | ZooKeeper path for replicated tables | +| ClickHouse | `weave-python env` | `WF_CLICKHOUSE_REPLICATED_CLUSTER` | string | None | Cluster name for replicated tables | +| ClickHouse | `weave-python env` | `WF_CLICKHOUSE_USE_DISTRIBUTED_TABLES` | string | `false` | Enable distributed tables | +| ClickHouse | `weave-python env` | `WF_CLICKHOUSE_CALLS_SHARD_KEY` | string | `trace_id` | Shard key (trace_id, id, or project_id) | +| ClickHouse | `weave-python env` | `WF_CLICKHOUSE_MAX_MEMORY_USAGE` | string | None | Max memory per query | +| ClickHouse | `weave-python env` | `WF_CLICKHOUSE_MAX_EXECUTION_TIME` | string | None | Max query execution time | +| ClickHouse | `weave-python env` | `WF_CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MIN_MS` | int | 100 | Async insert min busy timeout (ms) | +| ClickHouse | `weave-python env` | `WF_CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MAX_MS` | int | 1000 | Async insert max busy timeout (ms) | +| ClickHouse | `weave-python env` | `WEAVE_TRACE_CLICKHOUSE_USE_ASYNC_INSERT` | string | `true` | Enable async inserts | +| ClickHouse | `weave-python env` | `WF_CLICKHOUSE_DISABLE_LIGHTWEIGHT_UPDATE` | string | `false` | Disable lightweight UPDATE/DELETE | +| **S3-compatible** | `connectors.ConnectionInfo` | `scheme` | string | `s3` | Also: `cw` for CoreWeave | +| S3-compatible | `connectors.ConnectionInfo` | `host` | string | — | Empty for AWS default; set for self-hosted endpoints | +| S3-compatible | `connectors.ConnectionInfo` | `port` | int | — | e.g. 8333 for SeaweedFS, 9000 for MinIO | +| S3-compatible | `connectors.ConnectionInfo`<br>`weave-python env` | `username`<br>`WF_FILE_STORAGE_AWS_ACCESS_KEY_ID` | string | None | AWS Access Key ID | +| S3-compatible | `connectors.ConnectionInfo`<br>`weave-python env` | `password`<br>`WF_FILE_STORAGE_AWS_SECRET_ACCESS_KEY` | string | None | AWS Secret Access Key | +| S3-compatible | `connectors.ConnectionInfo` | `path (bucket)` | string | — | Bucket name + optional sub-path | +| S3-compatible | `connectors.S3QueryParams` | `tls` | bool | varies | Depends on service type detection | +| S3-compatible | `connectors.S3QueryParams` | `forcePathStyle` | bool | varies | false for AWS, true for S3-compat | +| S3-compatible | `connectors.S3QueryParams`<br>`weave-python env` | `region`<br>`WF_FILE_STORAGE_AWS_REGION` | string | `us-west-1` | AWS region override | +| S3-compatible | `weave-python env` | `WF_FILE_STORAGE_URI` | string | None | Storage bucket URI | +| S3-compatible | `weave-python env` | `WF_FILE_STORAGE_AWS_SESSION_TOKEN` | string | None | AWS temporary session token | +| S3-compatible | `weave-python env` | `WF_FILE_STORAGE_AWS_KMS_KEY` | string | None | KMS key ID for encryption | +| **GCS** | `connectors.ConnectionInfo` | `scheme` | string | `gs` | Google Cloud Storage; **`gcs://` is not accepted** | +| GCS | `connectors.ConnectionInfo` | `host (bucket)` | string | — | Bucket name (`gs://<bucket>[/path]`) | +| GCS | `connectors.ConnectionInfo` | `username`/`password` | string | None | Optional SA email + PEM private key; otherwise workload identity / `GOOGLE_APPLICATION_CREDENTIALS` | +| GCS | `weave-python env` | `WF_FILE_STORAGE_URI` | string | None | `gs://<bucket>` | +| **Azure** | `connectors.ConnectionInfo` | `scheme` | string | `az` | Azure Blob; **`azure://` is not accepted** | +| Azure | `connectors.ConnectionInfo` | `host (account)` | string | — | Storage account name (`az://<account>/<container>[/path]`) | +| Azure | `connectors.ConnectionInfo` | `path (container)` | string | — | Container name + optional sub-path | +| Azure | `connectors.ConnectionInfo` | `password` | string | None | Storage account key; falls back to `AZURE_STORAGE_KEY` / ambient identity | +| Azure | `weave-python env` | `WF_FILE_STORAGE_URI` | string | None | `az://<account>/<container>` | +| **Kafka** | `connectors.ConnectionInfo` | `scheme` | string | `kafka` | — | +| Kafka | `connectors.ConnectionInfo`<br>`weave-python env` | `host`<br>`KAFKA_BROKER_HOST` | string | `localhost` | Broker host | +| Kafka | `connectors.ConnectionInfo`<br>`weave-python env` | `port`<br>`KAFKA_BROKER_PORT` | int | 9092 | Broker port | +| Kafka | `connectors.ConnectionInfo`<br>`weave-python env` | `username`<br>`KAFKA_CLIENT_USER` | string | None | SASL_PLAIN username | +| Kafka | `connectors.ConnectionInfo`<br>`weave-python env` | `password`<br>`KAFKA_CLIENT_PASSWORD` | string | None | SASL_PLAIN password | +| Kafka | `connectors.ConnectionInfo` | `path (topic)` | string | — | Topic name (slashes become dots) | +| Kafka | `connectors.KafkaQueryParams` | `consumer_group_id` | string | — | Consumer group ID | +| Kafka | `connectors.KafkaQueryParams` | `min_bytes` | int32 | 1 | Consumer fetch min bytes | +| Kafka | `connectors.KafkaQueryParams` | `max_bytes` | int32 | 10MB | Consumer fetch max bytes | +| Kafka | `connectors.KafkaQueryParams` | `max_wait` | duration | 200ms | Consumer fetch max wait | +| Kafka | `connectors.KafkaQueryParams` | `request_timeout` | duration | 1s | Producer request timeout | +| Kafka | `connectors.KafkaQueryParams` | `linger` | duration | 10ms | Producer linger time | +| Kafka | `connectors.KafkaQueryParams` | `producer_batch_max_bytes` | int32 | 15MB | Producer max batch size | +| Kafka | `connectors.KafkaQueryParams` | `num_partitions` | int | 1 | Topic partition count | +| Kafka | `weave-python env` | `KAFKA_PRODUCER_MAX_BUFFER_SIZE` | string | None | Producer buffer size limit | +| Kafka | `weave-python env` | `KAFKA_PARTITION_BY_PROJECT_ID` | string | `false` | Partition by project ID | diff --git a/docs/monitoring.md b/docs/monitoring.md new file mode 100644 index 00000000..b8fac7e1 --- /dev/null +++ b/docs/monitoring.md @@ -0,0 +1,156 @@ +# Monitoring and Telemetry Guide + +This repo now ships a configurable telemetry stack for W&B. + +## Behavior + +- `telemetry.mode: off` + No telemetry stack resources are rendered and the operator does not wire OTEL endpoints. + +- `telemetry.mode: forward` + The Victoria stack is installed, workloads send OTEL data to the in-cluster gateway, and that gateway forwards OTLP data to a customer-managed endpoint. + +- `telemetry.mode: full` + The Victoria stack is installed and exposed through in-cluster Grafana dashboards and datasources. + +Installed resources in `forward` and `full` modes: +- `VMSingle`, `VMAgent`, `VLSingle`, `VTSingle` +- OTLP gateway collector (`victoria-otlp-gateway`) +- Scrapes (`VMNodeScrape`, `VMPodScrape`, `VMServiceScrape`) when `telemetry.scrape.enabled=true` +- Alerting (`VMRule`, `VMAlert`) when `telemetry.alerting.enabled=true` + +Installed resources only in `full` mode: +- Grafana + Victoria datasources + +Not installed: +- Perses +- vmui + +## Helm Values + +For the operator chart, set these under top-level `telemetry`: + +```yaml +telemetry: + mode: forward + namespace: wandb + otel: + secretName: wandb-otel-connection + protocol: http/protobuf + serviceName: wandb-service + resourceAttributes: deployment.environment=prod + forwarding: + otlp: + endpoint: https://otel.example.com + protocol: http/protobuf + headers: + Authorization: Bearer <token> +``` + +Notes: +- Retention defaults to `1d`. +- `telemetry.mode=forward/full` requires `telemetry.otel.secretName`. +- `telemetry.mode=forward` requires `telemetry.forwarding.otlp.endpoint`. +- The telemetry mode controls the stack behavior, but Helm still needs dependency booleans for the VictoriaMetrics and Grafana operator subcharts. +- When telemetry is enabled, Helm renders `wandb-operator-telemetry-config` in the operator release namespace. The operator reads that ConfigMap through the Kubernetes API and re-reconciles W&B instances when it changes. +- When telemetry is off, that ConfigMap is not rendered and the operator treats telemetry as disabled. +- Use the preset files in `deploy/operator/profiles/` to avoid remembering those extra flags and to switch modes cleanly on an existing release. +- `helm upgrade --install wandb-operator ./deploy/operator --reset-values -f ./deploy/operator/profiles/telemetry-full.yaml` installs the operator plus the full local telemetry stack. +- `helm upgrade --install wandb-operator ./deploy/operator --reset-values -f ./deploy/operator/profiles/telemetry-off.yaml` disables the telemetry stack and its dependent operators. +- `helm install telemetry ./deploy/telemetry --set mode=full --set namespace=<ns>` installs just the telemetry resources and expects the VictoriaMetrics/Grafana operators and CRDs to already exist. +- When rendering YAML manually, use `helm template <release> ./deploy/operator --include-crds ...` so the VictoriaMetrics and Grafana CRDs are present before apply. + +## Operator Runtime Values + +Set OTEL secret defaults under `telemetry.otel`: + +```yaml +telemetry: + otel: + secretName: wandb-otel-connection + protocol: http/protobuf + serviceName: wandb-service + resourceAttributes: deployment.environment=prod +``` + +## Tilt Usage + +Telemetry is off by default in Tilt. Set `"observabilityMode": "full"` in +`tilt-settings.star` for the local full stack. + +Tilt renders the operator chart with `telemetry.mode=full`, enables the +VictoriaMetrics and Grafana operator dependencies, and lets the controller read +the chart-rendered telemetry ConfigMap. + +Tilt exposes endpoints for: +- Grafana +- VictoriaMetrics +- VictoriaLogs +- VictoriaTraces + +## Manifest `source.type=telemetry` + +The resolved operator-managed telemetry status is published on +`WeightsAndBiases.status.telemetryStatus`, including `ready`, `state`, `mode`, +and nested `connection` details such as the effective protocol, endpoints, +Secret name, gorilla tracer connection, DogStatsD address, and local +Datadog-agent compatibility endpoint. These Datadog-compatible values point at +the in-cluster telemetry gateway; they do not add a Datadog SaaS exporter. + +```bash +kubectl get weightsandbiases <name> -n <namespace> -o jsonpath='{.status.telemetryStatus}' +``` + +```bash +kubectl get weightsandbiases <name> -n <namespace> -o jsonpath='{.status.telemetryStatus.connection.connectionSecret}' +``` + +You can source env vars from the operator-managed telemetry secret: + +```yaml +env: + - name: GORILLA_TRACER + sources: + - type: telemetry + field: gorillaTracer + - name: GORILLA_STATSD_ADDRESS + sources: + - type: telemetry + field: statsdAddress + - name: DD_TRACE_AGENT_URL + sources: + - type: telemetry + field: datadogTraceAgentURL + - name: DD_AGENT_HOST + sources: + - type: telemetry + field: datadogTraceAgentHost + - name: DD_TRACE_AGENT_PORT + sources: + - type: telemetry + field: datadogTraceAgentPort + - name: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT + sources: + - type: telemetry + field: metricsEndpoint + - name: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT + sources: + - type: telemetry + field: logsEndpoint + - name: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT + sources: + - type: telemetry + field: tracesEndpoint +``` + +## Validation Checklist + +1. Render stack: + - `helm template wandb-operator ./deploy/operator --include-crds -f ./deploy/operator/profiles/telemetry-full.yaml` + - or `helm template ./deploy/telemetry --set mode=full --set namespace=<telemetry-namespace>` +2. Verify Grafana resources: + - `kubectl get grafana,grafanadatasource -n <telemetry-namespace>` +3. Verify telemetry secret: + - `kubectl get secret <configured-secret-name> -n <wandb-namespace> -o yaml` +4. Verify pod env: + - `kubectl exec -n <wandb-namespace> deploy/<app> -- env | grep OTEL_` diff --git a/docs/openshift.md b/docs/openshift.md new file mode 100644 index 00000000..51312218 --- /dev/null +++ b/docs/openshift.md @@ -0,0 +1,201 @@ +# Deploying on OpenShift + +This guide covers installing the W&B operator on an OpenShift Container Platform +(OCP) cluster, the OpenShift-specific configuration the operator needs, and the +known limitations of running under OpenShift's default `restricted-v2` Security +Context Constraint (SCC). + +For **local** OpenShift development with CRC + Tilt, see +[`config/openshift-dev/README.md`](../config/openshift-dev/README.md) instead — +that path is automated and does not require the manual steps below. + +## Why OpenShift needs special handling + +OpenShift admits every pod through an SCC. The default `restricted-v2` SCC is +stricter than upstream Kubernetes defaults: it assigns each pod an **arbitrary +UID** from the namespace's `openshift.io/sa.scc.uid-range`, forbids running as a +fixed UID, drops all capabilities, disallows privileged ports (`<1024`), and +requires a `runtime/default` seccomp profile. + +Several components the operator manages ship images that assume a fixed UID or a +privileged port, so they must be adapted. The operator does this automatically +when it knows it is running on OpenShift, driven by two switches: + +| Switch | Where | Effect | +| --- | --- | --- | +| `OPENSHIFT=true` env on the operator | `profiles/openshift.yaml` | Makes `utils.IsOpenShift()` true, so managed infra specs omit fixed UID/GID and the Kafka pods get a dedicated SA bound to `nonroot-v2`. | +| `openshift.enabled=true` chart value | `profiles/openshift.yaml` | Renders the OpenShift-only RBAC/SCC templates (`openshift-owner-finalizers-rbac.yaml`, `openshift-scc-rbac.yaml`). | + +Both are set for you by the `profiles/openshift.yaml` values overlay. + +## Prerequisites + +- OpenShift 4.x cluster. +- `cluster-admin` (or equivalent) for the install: the chart creates + cluster-scoped RBAC and SCC grants. +- [`helm`](https://github.com/helm/helm) 3.x and [`oc`](https://formulae.brew.sh/formula/openshift-cli). +- A W&B server version and (for production) a container image registry the + cluster can pull from. + +## Known limitations + +| Component | Limitation | Status / workaround | +| --- | --- | --- | +| **Ingress / Frontend (`frontend-nginx`)** | The bundled frontend image runs as a fixed, non-numeric user (`nginx`) that owns `/usr/share/nginx/html` and rewrites files there at startup. `restricted-v2`'s arbitrary UID cannot write, and `nonroot-v2` rejects the pod because the kubelet cannot verify a non-numeric user is non-root. | **BYO ingress required.** Front W&B with your own ingress/route. | +| **Kafka (bufstream)** | The distroless broker image ships a `0700` binary owned by a fixed UID (65532) that can only be executed as that exact user, so it cannot run under `restricted-v2`. | Currently runs under `nonroot-v2` (via a dedicated ServiceAccount) rather than `restricted-v2`. | +| **Cluster-scoped install** | The chart creates SCC grants and cluster-scoped RBAC. | Requires `cluster-admin` at install time. | + +## Required: bring your own ingress + +On OpenShift you **must** supply your own ingress/edge. The bundled frontend does +not run under OpenShift's `restricted-v2` SCC (see +[Known limitations](#known-limitations)). + +- **Ingress (BYO required).** Front W&B with the cluster's own edge — an + OpenShift `Route` or your ingress controller. +- **Object storage (optional BYO).** Managed SeaweedFS runs under `restricted-v2` + (its S3 gateway binds an unprivileged port, so no root/`anyuid` grant is + needed). You can still point the CR at an external object store (S3, GCS, Azure + Blob, or any S3-compatible endpoint you run) via + `spec.objectStore.externalObjectStore` if you prefer. See + [Infrastructure Connection Settings](infra-connection-settings.md). + +The rest of the managed infra (MySQL, Redis, ClickHouse, Kafka) is supported on +OpenShift via the adaptations described below. + +## Deploying + +### 1. Install the operator with the OpenShift profile + +From a checkout of this repository: + +```bash +helm install wandb-operator ./deploy/operator \ + --namespace wandb-operators --create-namespace \ + -f deploy/operator/profiles/openshift.yaml +``` + +Installing from the published OCI chart works the same way, but you must supply +the OpenShift values yourself (the `-f` file must be a local path). Save the +snippet below as `openshift-values.yaml`: + +```yaml +openshift: + enabled: true +wandb-operator: + podSecurityContext: + runAsUser: null + runAsGroup: null + fsGroup: null + fsGroupChangePolicy: null + containers: + operator: + env: + OPENSHIFT: + value: "true" +redis-operator: + podSecurityContext: { runAsUser: null, runAsGroup: null, fsGroup: null, fsGroupChangePolicy: null } +altinity-clickhouse-operator: + podSecurityContext: { runAsUser: null, runAsGroup: null, fsGroup: null, fsGroupChangePolicy: null } +seaweedfs-operator: + podSecurityContext: { runAsUser: null, runAsGroup: null, fsGroup: null } +moco: + extraArgs: + - --disable-default-security-context +grafana-operator: + isOpenShift: true +``` + +```bash +helm install wandb-operator \ + oci://us-docker.pkg.dev/wandb-production/public/wandb/charts/operator \ + --namespace wandb-operators --create-namespace \ + -f openshift-values.yaml +``` + +### 2. Apply a `WeightsAndBiases` resource + +Object storage can be managed (SeaweedFS runs under `restricted-v2`) or external. +The example below wires an external object store; omit the `objectStore` block to +use the managed default. When bringing your own, provide the connection details +in a Secret and reference its keys: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: wandb-object-store + namespace: wandb +stringData: + bucket: my-wandb-bucket + region: us-east-1 + accessKey: <access-key> + secretKey: <secret-key> + # endpoint/port only for non-AWS, S3-compatible stores (e.g. MinIO) + # endpoint: minio.example.com + # port: "9000" +--- +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb + namespace: wandb +spec: + size: small + retentionPolicy: + onDelete: detach + wandb: + version: <wandb-version> + networking: + mode: ingress + objectStore: + externalObjectStore: + bucket: { name: wandb-object-store, key: bucket } + region: { name: wandb-object-store, key: region } + accessKey: { name: wandb-object-store, key: accessKey } + secretKey: { name: wandb-object-store, key: secretKey } + # endpoint: { name: wandb-object-store, key: endpoint } + # port: { name: wandb-object-store, key: port } +``` + +```bash +kubectl apply -f wandb.yaml +``` + +On OpenShift you must front the deployment with the cluster's own edge — an +OpenShift `Route` or your ingress controller — not a bundled load balancer or +the bundled frontend. See +[`docs/infra-connection-settings.md`](infra-connection-settings.md) for +networking and object-store connection options. + +### 3. Verify + +```bash +oc get pods -n wandb +``` + +Every managed pod should reach `Running`. You can confirm the SCC each pod was +admitted under with: + +```bash +oc get pods -n wandb \ + -o custom-columns=NAME:.metadata.name,SCC:'.metadata.annotations.openshift\.io/scc' +``` + +Managed infra pods run under `restricted-v2`, except the Kafka broker/etcd pods, +which run under `nonroot-v2` (see below). + +## What the OpenShift profile changes + +- **wandb-operator, crd-installer, and the dependency operators** (redis, + Altinity ClickHouse, SeaweedFS) drop their hardcoded `runAsUser`/`runAsGroup`/ + `fsGroup`, so OpenShift assigns compliant IDs at admission. +- **MySQL (moco)** runs the controller with `--disable-default-security-context` + so it does not inject a fixed UID/GID (10000) that `restricted-v2` rejects. +- **Kafka (bufstream)** gets a dedicated ServiceAccount bound to the + `nonroot-v2` SCC, because its distroless broker image ships a `0700` binary + owned by a fixed UID (65532) that can only be executed as that exact user. +- **OwnerReferencesPermissionEnforcement** RBAC is rendered so the built-in + StatefulSet controller (moco PVCs) can set finalizers on the resources it owns + — OpenShift's admission plugin requires this and it is a no-op on upstream + Kubernetes. diff --git a/docs/plan-ingress-gateway-api.md b/docs/plan-ingress-gateway-api.md new file mode 100644 index 00000000..8809306f --- /dev/null +++ b/docs/plan-ingress-gateway-api.md @@ -0,0 +1,990 @@ +# Plan: Ingress and Gateway API Support for WeightsAndBiases Operator + +## Context + +The W&B operator currently exposes services via NodePort (with an nginx-proxy feature flag) but has no support for Kubernetes Ingress or Gateway API resources. Users deploying W&B on-prem need standard ingress/routing to expose the application externally. The `ApplicationSpec` already defines an `IngressTemplate` field and `IngressStatus` in the status, but neither is wired up — no reconciliation logic exists. + +This plan adds a top-level networking configuration to the `WeightsAndBiasesSpec` CR, manifest-level route declarations per application, and reconciliation logic in the V2 reconciler (for Ingress and Gateway) and the Application controller (for HTTPRoute). Ingress and Gateway API are supported as a mutually exclusive choice. + +### Key architectural decisions + +**1. Ingress is a single consolidated resource, not per-application.** + +A Kubernetes Ingress naturally aggregates multiple path-based routing rules into a single resource. We create one Ingress for the entire W&B deployment with path rules routing to each application's Service backend. This is managed in `reconcileApplications()` inside `reconcile_v2.go` — the same place that already has visibility into all applications and their services. + +**2. HTTPRoutes are per-application.** + +Gateway API is designed for distributed route ownership — each team/service manages its own HTTPRoute. Each application gets its own HTTPRoute in the Application controller, attaching to the shared Gateway via `parentRef`. + +**3. Managed vs. External Gateway.** + +When the user selects Gateway API mode, the operator supports two sub-modes: + +- **Managed Gateway** (`gateway.managed: true`): The operator creates and reconciles a `Gateway` resource in `ReconcileWandbManifest()` alongside other shared resources (service accounts, roles, etc.). The operator owns the full lifecycle — create, update, and delete on CR teardown. +- **External Gateway** (`gateway.managed: false` or omitted): The operator only creates `HTTPRoute` resources that attach to a user-provisioned `Gateway` via `gatewayRef`. The operator never touches the `Gateway` itself. + +**4. Resource ownership summary.** + +| Resource | Where reconciled | Cardinality | +|----------|-----------------|-------------| +| Ingress | `reconcile_v2.go` (`reconcileApplications()`) | One per W&B deployment | +| Gateway | `reconcile_v2.go` (`ReconcileWandbManifest()`) | One per W&B deployment (managed only) | +| HTTPRoute | `application_controller.go` | One per application | + +--- + +## Step 1: Add Gateway API dependency + +**File:** `go.mod` + +Run `go get sigs.k8s.io/gateway-api` to add the Gateway API types. The project already uses `k8s.io/api v0.35.0` and `sigs.k8s.io/controller-runtime v0.22.4`, so Gateway API v1.2+ should be compatible. + +--- + +## Step 2: Add Networking types to the WeightsAndBiases CR + +**File:** `api/v2/weightsandbiases_types.go` + +Add a `Networking` field to `WeightsAndBiasesSpec` and define the supporting types: + +```go +type NetworkingMode string + +const ( + NetworkingModeNone NetworkingMode = "" + NetworkingModeIngress NetworkingMode = "Ingress" + NetworkingModeGatewayAPI NetworkingMode = "GatewayAPI" +) + +type NetworkingSpec struct { + // Mode selects the networking strategy: "Ingress" or "GatewayAPI". + // Empty/unset means no operator-managed ingress (preserves current NodePort behavior). + // +kubebuilder:validation:Enum="";Ingress;GatewayAPI + Mode NetworkingMode `json:"mode,omitempty"` + + // Ingress configures Kubernetes Ingress resources. Only used when Mode=Ingress. + // +optional + Ingress *IngressConfig `json:"ingress,omitempty"` + + // GatewayAPI configures Gateway API resources. Only used when Mode=GatewayAPI. + // +optional + GatewayAPI *GatewayAPIConfig `json:"gatewayAPI,omitempty"` + + // TLS configures TLS termination for both Ingress and Gateway API modes. + // +optional + TLS *TLSConfig `json:"tls,omitempty"` + + // Annotations applied to all generated Ingress or HTTPRoute resources. + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +type IngressConfig struct { + // IngressClassName sets the spec.ingressClassName field on Ingress resources. + // +optional + IngressClassName *string `json:"ingressClassName,omitempty"` +} + +type GatewayAPIConfig struct { + // Gateway configures the Gateway resource. Specifies either a managed + // Gateway (created by the operator) or a reference to an external one. + Gateway GatewayConfig `json:"gateway"` + + // ListenerName is the specific listener on the Gateway to attach HTTPRoutes to. + // When the Gateway is managed, this should match one of the listeners defined + // in the managed Gateway spec. When using an external Gateway, this selects + // which listener on that Gateway to target. + // +optional + ListenerName *string `json:"listenerName,omitempty"` +} + +type GatewayConfig struct { + // Managed controls whether the operator creates and manages the Gateway resource. + // When true, the operator creates a Gateway using the configuration below. + // When false (default), gatewayRef must be set to reference an existing Gateway. + // +kubebuilder:default=false + Managed bool `json:"managed,omitempty"` + + // GatewayRef references an existing external Gateway. Required when managed=false. + // +optional + GatewayRef *GatewayReference `json:"gatewayRef,omitempty"` + + // GatewayClassName is the name of the GatewayClass to use. + // Required when managed=true. + // +optional + GatewayClassName *string `json:"gatewayClassName,omitempty"` + + // Listeners defines the listeners on the managed Gateway. + // If empty and managed=true, a default HTTPS listener is created using + // the hostname from spec.wandb.hostname and the TLS secret from spec.networking.tls. + // +optional + Listeners []GatewayListener `json:"listeners,omitempty"` + + // Annotations applied to the managed Gateway resource. + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +type GatewayReference struct { + // Name of the Gateway resource. + Name string `json:"name"` + // Namespace of the Gateway resource. Defaults to the CR namespace if empty. + // +optional + Namespace string `json:"namespace,omitempty"` +} + +// GatewayListener defines a listener on a managed Gateway. +// This is a simplified view of gatewayv1.Listener — the operator +// constructs the full Listener from these fields plus CR-level TLS config. +type GatewayListener struct { + // Name is the listener name (e.g., "https", "http"). + Name string `json:"name"` + // Port is the network port (e.g., 443, 80). + Port int32 `json:"port"` + // Protocol is the listener protocol (e.g., "HTTPS", "HTTP"). + Protocol string `json:"protocol"` + // Hostname restricts this listener to a specific hostname. Optional. + // +optional + Hostname *string `json:"hostname,omitempty"` + // TLS configures TLS for this listener. If nil and protocol is HTTPS, + // the operator uses the top-level spec.networking.tls configuration. + // +optional + TLS *ListenerTLSConfig `json:"tls,omitempty"` +} + +type ListenerTLSConfig struct { + // Mode is the TLS mode (Terminate, Passthrough). Defaults to Terminate. + // +optional + Mode *string `json:"mode,omitempty"` + // CertificateRef references the Secret containing the TLS certificate. + // If nil, falls back to spec.networking.tls.secretName. + // +optional + CertificateRef *SecretRef `json:"certificateRef,omitempty"` +} + +type SecretRef struct { + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` +} + +type TLSConfig struct { + // SecretName is the name of a kubernetes.io/tls Secret for TLS termination. + // +optional + SecretName string `json:"secretName,omitempty"` + + // CertManager enables cert-manager annotations on Ingress resources. + // +optional + CertManager *CertManagerConfig `json:"certManager,omitempty"` +} + +type CertManagerConfig struct { + // ClusterIssuer is the name of the cert-manager ClusterIssuer to use. + // +optional + ClusterIssuer string `json:"clusterIssuer,omitempty"` + // Issuer is the name of the cert-manager Issuer (namespace-scoped) to use. + // +optional + Issuer string `json:"issuer,omitempty"` +} +``` + +Add field to `WeightsAndBiasesSpec`: +```go +type WeightsAndBiasesSpec struct { + // ... existing fields ... + + // Networking configures how the W&B application is exposed externally. + // +optional + Networking NetworkingSpec `json:"networking,omitempty"` +} +``` + +--- + +## Step 3: Add HTTPRouteTemplate to Application types + +**File:** `api/v2/application_types.go` + +Add `HTTPRouteTemplate` for Gateway API. This is per-application since Gateway API supports distributed route ownership. The existing `IngressTemplate` field is left in place but remains unused — Ingress is managed as a consolidated resource at the V2 reconciler level (see Step 5). + +```go +import ( + // add: gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +type ApplicationSpec struct { + // ... existing fields ... + + // HTTPRouteTemplate is the desired HTTPRoute spec. Nil means no HTTPRoute. + // +optional + HTTPRouteTemplate *HTTPRouteTemplateSpec `json:"httpRouteTemplate,omitempty"` +} + +// HTTPRouteTemplateSpec wraps the key fields needed to build an HTTPRoute +// rather than embedding the full upstream spec (which has server-managed fields). +type HTTPRouteTemplateSpec struct { + ParentRefs []gatewayv1.ParentReference `json:"parentRefs"` + Hostnames []gatewayv1.Hostname `json:"hostnames,omitempty"` + Rules []gatewayv1.HTTPRouteRule `json:"rules,omitempty"` +} + +type ApplicationStatus struct { + // ... existing fields ... + + // HTTPRouteStatus summarizes the observed state of the managed HTTPRoute. + // +optional + HTTPRouteStatus *HTTPRouteStatusSummary `json:"httpRouteStatus,omitempty"` +} + +type HTTPRouteStatusSummary struct { + // Accepted indicates if the HTTPRoute was accepted by the Gateway. + Accepted bool `json:"accepted,omitempty"` +} +``` + +--- + +## Step 4: Add route declarations to the Manifest + +**File:** `pkg/wandb/manifest/manifest.go` + +Add an optional `Ingress` field to the manifest `Application` struct so server manifests can declare per-application routing needs. This is used by both the consolidated Ingress builder (Step 5c) and the per-app HTTPRoute builder (Step 5d). + +```go +type Application struct { + // ... existing fields ... + + // Ingress declares per-application routing configuration. + // Used by the operator to build Ingress path rules or HTTPRoute resources + // when networking is enabled in the CR spec. + Ingress *AppIngressSpec `yaml:"ingress,omitempty"` +} + +type AppIngressSpec struct { + // Paths defines the URL path prefixes this application should serve. + // Defaults to ["/"] if empty. + Paths []string `yaml:"paths,omitempty"` + + // ServicePort is the name or number of the backend service port. + // If empty, the first port from the Service spec is used. + ServicePort string `yaml:"servicePort,omitempty"` + + // PathType controls path matching semantics ("Prefix", "Exact", "ImplementationSpecific"). + // Defaults to "Prefix". + PathType string `yaml:"pathType,omitempty"` +} +``` + +--- + +## Step 5: Networking reconciliation in reconcile_v2.go + +**File:** `internal/controller/v2/reconcile_v2.go` and new file `internal/controller/v2/gateway.go` + +### 5a: Reconcile managed Gateway in ReconcileWandbManifest + +The Gateway is a shared namespace-level resource. It is reconciled in `ReconcileWandbManifest()` after service account setup but before `reconcileApplications()`: + +```go +// In ReconcileWandbManifest(), after createOrUpdateRoleBinding() and before runMigrations(): +if wandb.Spec.Networking.Mode == apiv2.NetworkingModeGatewayAPI { + if err := reconcileGateway(ctx, client, wandb); err != nil { + logger.Error(err, "Failed to reconcile Gateway") + return ctrl.Result{}, err + } +} +``` + +New file `internal/controller/v2/gateway.go`: + +```go +func reconcileGateway(ctx context.Context, c ctrlClient.Client, wandb *apiv2.WeightsAndBiases) error { + gwConfig := wandb.Spec.Networking.GatewayAPI.Gateway + + if !gwConfig.Managed { + if gwConfig.GatewayRef == nil { + return fmt.Errorf("gatewayAPI.gateway.gatewayRef is required when managed=false") + } + return validateExternalGatewayExists(ctx, c, wandb, gwConfig.GatewayRef) + } + + // Managed gateway: build and reconcile + gatewayName := fmt.Sprintf("%s-gateway", wandb.Name) + hostname := parseHostname(wandb.Spec.Wandb.Hostname) + + desired := &gatewayv1.Gateway{} + desired.Name = gatewayName + desired.Namespace = wandb.Namespace + desired.Spec.GatewayClassName = gatewayv1.ObjectName(*gwConfig.GatewayClassName) + + if len(gwConfig.Listeners) > 0 { + desired.Spec.Listeners = buildListenersFromConfig(gwConfig.Listeners, wandb) + } else { + desired.Spec.Listeners = buildDefaultListeners(hostname, wandb) + } + + desired.Labels = map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + } + desired.Annotations = gwConfig.Annotations + + if err := controllerutil.SetOwnerReference(wandb, desired, c.Scheme()); err != nil { + return err + } + + current := &gatewayv1.Gateway{} + err := c.Get(ctx, types.NamespacedName{Name: gatewayName, Namespace: wandb.Namespace}, current) + if err != nil { + if apiErrors.IsNotFound(err) { + return c.Create(ctx, desired) + } + return err + } + + desired.ResourceVersion = current.ResourceVersion + return c.Update(ctx, desired) +} + +func deleteGateway(ctx context.Context, c ctrlClient.Client, wandb *apiv2.WeightsAndBiases) error { + gatewayName := fmt.Sprintf("%s-gateway", wandb.Name) + gw := &gatewayv1.Gateway{} + if err := c.Get(ctx, types.NamespacedName{Name: gatewayName, Namespace: wandb.Namespace}, gw); err != nil { + if apiErrors.IsNotFound(err) { + return nil + } + return err + } + return c.Delete(ctx, gw) +} + +func validateExternalGatewayExists(ctx context.Context, c ctrlClient.Client, wandb *apiv2.WeightsAndBiases, ref *apiv2.GatewayReference) error { + ns := ref.Namespace + if ns == "" { + ns = wandb.Namespace + } + gw := &gatewayv1.Gateway{} + return c.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: ns}, gw) +} +``` + +Helper functions `buildListenersFromConfig()` and `buildDefaultListeners()` translate CR config into `gatewayv1.Listener` structs: +- Map `GatewayListener.Protocol` → `gatewayv1.ProtocolType` +- Map `GatewayListener.TLS` → `gatewayv1.GatewayTLSConfig` with certificate refs +- For the default listener: use hostname from `spec.wandb.hostname`, port 443, protocol HTTPS, TLS terminate mode, and secret from `spec.networking.tls.secretName` + +### 5b: Add Gateway cleanup to finalization + +In the `Reconcile()` function's deletion/finalization block (lines 85-116), add Gateway cleanup before the finalizer is removed: + +```go +if wandb.Spec.Networking.Mode == apiv2.NetworkingModeGatewayAPI && + wandb.Spec.Networking.GatewayAPI != nil && + wandb.Spec.Networking.GatewayAPI.Gateway.Managed { + if err = deleteGateway(ctx, client, wandb); err != nil { + return ctrl.Result{}, err + } +} +``` + +### 5c: Build consolidated Ingress in reconcileApplications + +The Ingress is built **after** the application loop in `reconcileApplications()`, since it needs to aggregate path rules from all applications. This follows the same pattern as the existing hostname resolution block (lines 408-429) which also runs after all apps are processed. + +```go +func reconcileApplications(...) (ctrl.Result, error) { + // ... existing application loop (lines 315-406) ... + + // After the application loop, build consolidated networking resources + if wandb.Spec.Networking.Mode == apiv2.NetworkingModeIngress { + if err := reconcileConsolidatedIngress(ctx, client, wandb, manifest); err != nil { + logger.Error(err, "Failed to reconcile Ingress") + return ctrl.Result{}, err + } + } + + // ... existing hostname resolution block (lines 408-429) ... +} +``` + +New file `internal/controller/v2/ingress.go`: + +```go +func reconcileConsolidatedIngress(ctx context.Context, c ctrlClient.Client, wandb *apiv2.WeightsAndBiases, manifest serverManifest.Manifest) error { + ingressName := fmt.Sprintf("%s-ingress", wandb.Name) + hostname := parseHostname(wandb.Spec.Wandb.Hostname) + ingressConfig := wandb.Spec.Networking.Ingress + + // Build path rules from all applications that declare routing + var rules []networkingv1.IngressRule + var paths []networkingv1.HTTPIngressPath + + for _, app := range manifest.Applications { + if len(app.Features) > 0 && !manifestFeaturesEnabled(app.Features, manifest.Features) { + continue + } + if app.Ingress == nil && app.Service == nil { + continue + } + + appPaths := []string{"/"} + pathType := networkingv1.PathTypePrefix + if app.Ingress != nil { + if len(app.Ingress.Paths) > 0 { + appPaths = app.Ingress.Paths + } + if app.Ingress.PathType == "Exact" { + pathType = networkingv1.PathTypeExact + } else if app.Ingress.PathType == "ImplementationSpecific" { + pathType = networkingv1.PathTypeImplementationSpecific + } + } + + serviceName := fmt.Sprintf("%s-%s", wandb.Name, app.Name) + servicePort := resolveServicePort(app) + + for _, p := range appPaths { + paths = append(paths, networkingv1.HTTPIngressPath{ + Path: p, + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: serviceName, + Port: servicePort, + }, + }, + }) + } + } + + // Build rules for primary hostname + rules = append(rules, networkingv1.IngressRule{ + Host: hostname, + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{Paths: paths}, + }, + }) + + // Add rules for additional hostnames (same paths, different host) + for _, additionalHost := range wandb.Spec.Wandb.AdditionalHostnames { + rules = append(rules, networkingv1.IngressRule{ + Host: additionalHost, + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{Paths: paths}, + }, + }) + } + + // Build the Ingress + desired := &networkingv1.Ingress{} + desired.Name = ingressName + desired.Namespace = wandb.Namespace + desired.Spec.IngressClassName = ingressConfig.IngressClassName + desired.Spec.Rules = rules + + // TLS configuration + if wandb.Spec.Networking.TLS != nil && wandb.Spec.Networking.TLS.SecretName != "" { + allHosts := []string{hostname} + allHosts = append(allHosts, wandb.Spec.Wandb.AdditionalHostnames...) + desired.Spec.TLS = []networkingv1.IngressTLS{{ + Hosts: allHosts, + SecretName: wandb.Spec.Networking.TLS.SecretName, + }} + } + + // Labels + desired.Labels = map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + } + + // Annotations: merge networking-level + cert-manager annotations + desired.Annotations = map[string]string{} + for k, v := range wandb.Spec.Networking.Annotations { + desired.Annotations[k] = v + } + if wandb.Spec.Networking.TLS != nil && wandb.Spec.Networking.TLS.CertManager != nil { + cm := wandb.Spec.Networking.TLS.CertManager + if cm.ClusterIssuer != "" { + desired.Annotations["cert-manager.io/cluster-issuer"] = cm.ClusterIssuer + } + if cm.Issuer != "" { + desired.Annotations["cert-manager.io/issuer"] = cm.Issuer + } + } + + if err := controllerutil.SetOwnerReference(wandb, desired, c.Scheme()); err != nil { + return err + } + + // Create or Update + current := &networkingv1.Ingress{} + err := c.Get(ctx, types.NamespacedName{Name: ingressName, Namespace: wandb.Namespace}, current) + if err != nil { + if apiErrors.IsNotFound(err) { + return c.Create(ctx, desired) + } + return err + } + + desired.ResourceVersion = current.ResourceVersion + return c.Update(ctx, desired) +} + +func deleteConsolidatedIngress(ctx context.Context, c ctrlClient.Client, wandb *apiv2.WeightsAndBiases) error { + ingressName := fmt.Sprintf("%s-ingress", wandb.Name) + ingress := &networkingv1.Ingress{} + if err := c.Get(ctx, types.NamespacedName{Name: ingressName, Namespace: wandb.Namespace}, ingress); err != nil { + if apiErrors.IsNotFound(err) { + return nil + } + return err + } + return c.Delete(ctx, ingress) +} +``` + +Add Ingress cleanup to the finalization block alongside Gateway: + +```go +if wandb.Spec.Networking.Mode == apiv2.NetworkingModeIngress { + if err = deleteConsolidatedIngress(ctx, client, wandb); err != nil { + return ctrl.Result{}, err + } +} +``` + +### 5d: Populate HTTPRouteTemplate per application + +Inside the existing application loop in `reconcileApplications()`, after the `ServiceTemplate` block (~line 380-388), populate the HTTPRoute template for Gateway API mode: + +```go +// After ServiceTemplate population: +if wandb.Spec.Networking.Mode == apiv2.NetworkingModeGatewayAPI && app.Ingress != nil { + application.Spec.HTTPRouteTemplate = buildHTTPRouteTemplate(wandb, app) +} else { + application.Spec.HTTPRouteTemplate = nil +} +``` + +```go +func buildHTTPRouteTemplate(wandb *apiv2.WeightsAndBiases, app serverManifest.Application) *apiv2.HTTPRouteTemplateSpec { + gwConfig := wandb.Spec.Networking.GatewayAPI + + // Determine the parent ref + var parentRef gatewayv1.ParentReference + if gwConfig.Gateway.Managed { + gatewayName := gatewayv1.ObjectName(fmt.Sprintf("%s-gateway", wandb.Name)) + parentRef = gatewayv1.ParentReference{Name: gatewayName} + } else { + ns := gatewayv1.Namespace(gwConfig.Gateway.GatewayRef.Namespace) + parentRef = gatewayv1.ParentReference{ + Name: gatewayv1.ObjectName(gwConfig.Gateway.GatewayRef.Name), + Namespace: &ns, + } + } + if gwConfig.ListenerName != nil { + sectionName := gatewayv1.SectionName(*gwConfig.ListenerName) + parentRef.SectionName = §ionName + } + + // Build hostnames + hostname := parseHostname(wandb.Spec.Wandb.Hostname) + hostnames := []gatewayv1.Hostname{gatewayv1.Hostname(hostname)} + for _, h := range wandb.Spec.Wandb.AdditionalHostnames { + hostnames = append(hostnames, gatewayv1.Hostname(h)) + } + + // Build rules from manifest paths + appPaths := []string{"/"} + if app.Ingress != nil && len(app.Ingress.Paths) > 0 { + appPaths = app.Ingress.Paths + } + + serviceName := fmt.Sprintf("%s-%s", wandb.Name, app.Name) + servicePort := resolveServicePort(app) + + var matches []gatewayv1.HTTPRouteMatch + for _, p := range appPaths { + matchType := gatewayv1.PathMatchPathPrefix + if app.Ingress != nil && app.Ingress.PathType == "Exact" { + matchType = gatewayv1.PathMatchExact + } + matches = append(matches, gatewayv1.HTTPRouteMatch{ + Path: &gatewayv1.HTTPPathMatch{ + Type: &matchType, + Value: &p, + }, + }) + } + + backendRef := gatewayv1.HTTPBackendRef{ + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Name: gatewayv1.ObjectName(serviceName), + Port: &servicePort, + }, + }, + } + + return &apiv2.HTTPRouteTemplateSpec{ + ParentRefs: []gatewayv1.ParentReference{parentRef}, + Hostnames: hostnames, + Rules: []gatewayv1.HTTPRouteRule{{ + Matches: matches, + BackendRefs: []gatewayv1.HTTPBackendRef{backendRef}, + }}, + } +} +``` + +### 5e: Guard NodePort hostname override + +Modify the hostname resolution block (lines 408-429): skip the NodePort proxy hostname override when `Networking.Mode != ""`: + +```go +if wandb.Spec.Networking.Mode == apiv2.NetworkingModeNone { + // Existing NodePort proxy hostname logic + if manifestFeaturesEnabled([]string{"proxy"}, manifest.Features) && hostname.Port() == "" { + // ... existing NodePort discovery ... + } +} +``` + +--- + +## Step 6: Implement reconcileHTTPRoute() in the Application controller + +**File:** `internal/controller/application_controller.go` + +Only HTTPRoute is managed per-application. Ingress is not — it's a consolidated resource handled in Step 5c. + +Following the `reconcileService()` pattern (lines 714-804): + +```go +func (r *ApplicationReconciler) reconcileHTTPRoute(ctx context.Context, app *wandbv2.Application) error { + if app.Spec.HTTPRouteTemplate == nil { + return r.deleteHTTPRoute(ctx, app) + } + + logger := logx.GetSlog(ctx) + + desired := &gatewayv1.HTTPRoute{} + desired.Name = app.Name + desired.Namespace = app.Namespace + + desired.Labels = utils.MergeMapsStringString(desired.Labels, app.Spec.MetaTemplate.Labels) + desired.Annotations = utils.MergeMapsStringString(desired.Annotations, app.Spec.MetaTemplate.Annotations) + + desired.Spec.ParentRefs = app.Spec.HTTPRouteTemplate.ParentRefs + desired.Spec.Hostnames = app.Spec.HTTPRouteTemplate.Hostnames + desired.Spec.Rules = app.Spec.HTTPRouteTemplate.Rules + + current := &gatewayv1.HTTPRoute{} + err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, current) + if err != nil { + if !errors.IsNotFound(err) { + logger.Error("Failed to get HTTPRoute", logx.ErrAttr(err)) + return err + } + logger.Info("Creating HTTPRoute", "HTTPRoute", desired.Name) + return r.Create(ctx, desired) + } + + desired.ResourceVersion = current.ResourceVersion + logger.Info("Updating HTTPRoute", "HTTPRoute", desired.Name) + return r.Update(ctx, desired) +} + +func (r *ApplicationReconciler) deleteHTTPRoute(ctx context.Context, app *wandbv2.Application) error { + logger := logx.GetSlog(ctx) + route := &gatewayv1.HTTPRoute{} + if err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, route); err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + deletePolicy := client.PropagationPolicy(v1.DeletePropagationBackground) + if err := r.Delete(ctx, route, deletePolicy); err != nil { + logger.Error("Failed to delete HTTPRoute", logx.ErrAttr(err), "HTTPRoute", app.Name) + return err + } + logger.Info("Successfully deleted HTTPRoute", "HTTPRoute", app.Name) + return nil +} +``` + +Wire into `Reconcile()` after `reconcileHPA()` (~line 196): +```go +if err := r.reconcileHTTPRoute(ctx, &app); err != nil { + logger.Error("Failed to reconcile HTTPRoute", logx.ErrAttr(err)) + return ctrl.Result{}, err +} +``` + +Wire into finalization block (~line 116-126): +```go +if err := r.deleteHTTPRoute(ctx, &app); err != nil { + logger.Error("Failed to delete HTTPRoute during finalization", logx.ErrAttr(err)) + return ctrl.Result{}, err +} +``` + +Update `SetupWithManager()` to add `Owns(&gatewayv1.HTTPRoute{})`. + +--- + +## Step 7: Add RBAC markers + +**File:** `internal/controller/application_controller.go` (for HTTPRoute): + +```go +// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes/status,verbs=get +``` + +**File:** `internal/controller/weightsandbiases_controller.go` (for Ingress and Gateway): + +```go +// +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses/status,verbs=get +// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways/status,verbs=get +``` + +--- + +## Step 8: Add webhook validation + +**File:** `internal/webhook/v2/weightsandbiases_webhook.go` + +### Validation (add to `validateSpec`): +- If `Networking.Mode == "Ingress"` and `Networking.GatewayAPI` is set → error (mutual exclusivity) +- If `Networking.Mode == "GatewayAPI"` and `Networking.Ingress` is set → error +- If `Networking.Mode == "GatewayAPI"` and `Networking.GatewayAPI` is nil → error (config required) +- If `Networking.Mode == "GatewayAPI"` and `Gateway.Managed == false` and `Gateway.GatewayRef == nil` → error (must provide a reference) +- If `Networking.Mode == "GatewayAPI"` and `Gateway.Managed == true` and `Gateway.GatewayClassName == nil` → error (class required for managed) +- If `Networking.Mode == "GatewayAPI"` and `Gateway.Managed == true` and `Gateway.GatewayRef != nil` → error (conflicting: both managed and external) +- If `Networking.TLS.CertManager` is set and `Networking.Mode != "Ingress"` → warning (cert-manager annotations only apply to Ingress) + +### Defaulting: +- No aggressive defaults — empty `Networking` means NodePort behavior is preserved +- If `Gateway.Managed == true` and no listeners are specified, a default HTTPS listener is generated at reconcile time (not in the webhook) + +--- + +## Step 9: Add Gateway and Ingress status to WeightsAndBiasesStatus + +**File:** `api/v2/weightsandbiases_types.go` + +Add status tracking for networking resources: + +```go +type WeightsAndBiasesStatus struct { + // ... existing fields ... + + // GatewayStatus reports the state of the managed Gateway (if any). + // +optional + GatewayStatus *GatewayStatusSummary `json:"gatewayStatus,omitempty"` + + // IngressStatus reports the state of the consolidated Ingress (if any). + // +optional + IngressStatus *IngressStatusSummary `json:"ingressStatus,omitempty"` +} + +type GatewayStatusSummary struct { + // Name of the managed Gateway resource. + Name string `json:"name,omitempty"` + // Ready indicates whether the Gateway has been accepted and programmed. + Ready bool `json:"ready,omitempty"` + // Addresses lists the addresses assigned to the Gateway by the infrastructure. + Addresses []string `json:"addresses,omitempty"` +} + +type IngressStatusSummary struct { + // Name of the consolidated Ingress resource. + Name string `json:"name,omitempty"` + // LoadBalancer contains the current status of the load-balancer. + LoadBalancerIngress []corev1.LoadBalancerIngress `json:"loadBalancerIngress,omitempty"` +} +``` + +`reconcileConsolidatedIngress()` and `reconcileGateway()` update these status fields after each reconcile. + +--- + +## Step 10: Run code generation and update CRDs + +After all type changes: +```bash +make generate # deepcopy, etc. +make manifests # CRD YAMLs, RBAC +``` + +--- + +## Step 11: Tests + +- **Unit tests for `reconcileGateway()`** — test managed create/update, external validation, cleanup on delete +- **Unit tests for `reconcileConsolidatedIngress()`** — test single-app, multi-app path aggregation, TLS config, cert-manager annotations, additional hostnames, cleanup on delete +- **Unit tests for `buildHTTPRouteTemplate()`** — test managed gateway ref, external gateway ref, path mapping, listener name selection +- **Unit tests for `reconcileHTTPRoute()`** — create, update, delete (follow existing reconcileService test patterns) +- **Webhook validation tests** — mutual exclusivity, managed vs external validation, required fields +- Run `make lint` and `make test` + +--- + +## Files to modify (summary) + +| File | Change | +|------|--------| +| `go.mod` | Add `sigs.k8s.io/gateway-api` dependency | +| `api/v2/weightsandbiases_types.go` | Add `NetworkingSpec`, `GatewayAPIConfig`, `GatewayConfig`, `GatewayListener`, `GatewayStatusSummary`, `IngressStatusSummary` and related types; add `Networking` field to `WeightsAndBiasesSpec`; add `GatewayStatus`/`IngressStatus` to `WeightsAndBiasesStatus` | +| `api/v2/application_types.go` | Add `HTTPRouteTemplateSpec`, `HTTPRouteStatusSummary`, add `HTTPRouteTemplate` to `ApplicationSpec` | +| `pkg/wandb/manifest/manifest.go` | Add `AppIngressSpec` type, add `Ingress` field to `Application` | +| `internal/controller/v2/gateway.go` | **New file:** `reconcileGateway()`, `deleteGateway()`, `validateExternalGatewayExists()`, listener builder helpers | +| `internal/controller/v2/ingress.go` | **New file:** `reconcileConsolidatedIngress()`, `deleteConsolidatedIngress()`, path aggregation logic | +| `internal/controller/v2/reconcile_v2.go` | Call `reconcileGateway()` from `ReconcileWandbManifest()`, call `reconcileConsolidatedIngress()` from `reconcileApplications()`, add `buildHTTPRouteTemplate()` + populate per-app HTTPRouteTemplate, guard NodePort hostname logic, add cleanup to finalization | +| `internal/controller/application_controller.go` | Add `reconcileHTTPRoute()`, `deleteHTTPRoute()`, RBAC markers for HTTPRoute, wire into `Reconcile()` and finalization, update `SetupWithManager()` | +| `internal/controller/weightsandbiases_controller.go` | Add RBAC markers for Ingress and Gateway resources | +| `internal/webhook/v2/weightsandbiases_webhook.go` | Add `validateNetworkingSpec()`, wire into `validateSpec()` | +| `api/v2/zz_generated.deepcopy.go` | Auto-generated by `make generate` | +| `config/crd/bases/*.yaml` | Auto-generated by `make manifests` | +| `deploy/operator/crds/*.yaml` | Sync updated CRDs | + +--- + +## Example CR usage + +### Ingress mode +```yaml +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + size: small + wandb: + hostname: https://wandb.example.com + version: "0.76.1" + networking: + mode: Ingress + ingress: + ingressClassName: nginx + tls: + secretName: wandb-tls + certManager: + clusterIssuer: letsencrypt-prod + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: "0" +``` + +This creates a single Ingress `wandb-ingress` with path rules for each application: +``` +wandb.example.com/ → wandb-app:8080 +wandb.example.com/api/ → wandb-gorilla:8080 +wandb.example.com/console → wandb-console:8082 +... +``` + +### Gateway API — managed Gateway +```yaml +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + size: small + wandb: + hostname: https://wandb.example.com + version: "0.76.1" + networking: + mode: GatewayAPI + gatewayAPI: + gateway: + managed: true + gatewayClassName: istio + listeners: + - name: https + port: 443 + protocol: HTTPS + annotations: + networking.istio.io/service-type: ClusterIP + listenerName: https + tls: + secretName: wandb-tls +``` + +This creates one managed Gateway `wandb-gateway` and individual HTTPRoutes per application: +- `wandb-app` HTTPRoute: `wandb.example.com/` → `wandb-app:8080` +- `wandb-gorilla` HTTPRoute: `wandb.example.com/api/` → `wandb-gorilla:8080` +- etc. + +### Gateway API — external Gateway +```yaml +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + size: small + wandb: + hostname: https://wandb.example.com + version: "0.76.1" + networking: + mode: GatewayAPI + gatewayAPI: + gateway: + gatewayRef: + name: shared-gateway + namespace: gateway-system + listenerName: https + tls: + secretName: wandb-tls +``` + +### Gateway API — managed Gateway with defaults +```yaml +# Minimal config: operator creates a Gateway with a default HTTPS listener +# derived from spec.wandb.hostname and spec.networking.tls.secretName +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + size: small + wandb: + hostname: https://wandb.example.com + version: "0.76.1" + networking: + mode: GatewayAPI + gatewayAPI: + gateway: + managed: true + gatewayClassName: istio + tls: + secretName: wandb-tls +``` + +--- + +## Backward compatibility + +- An empty or unset `networking` field preserves current behavior (NodePort with optional nginx-proxy) +- The NodePort proxy hostname override in `reconcile_v2.go` is skipped only when `Networking.Mode != ""` +- No changes to existing CRD fields or defaults +- The existing `IngressTemplate` field on `ApplicationSpec` is left in place but remains unused for this feature — it could be used for future per-application Ingress overrides if needed + +--- + +## Verification + +1. `make generate` — ensure deepcopy generation succeeds +2. `make manifests` — ensure CRD and RBAC generation succeeds +3. `make lint` — no lint errors +4. `make test` — all tests pass including new ones +5. Manual: apply a CR with `networking.mode: Ingress` and verify a single Ingress with path rules for all apps is created +6. Manual: apply a CR with `networking.mode: GatewayAPI` + `gateway.managed: true` and verify a Gateway + per-app HTTPRoutes are created +7. Manual: apply a CR with `networking.mode: GatewayAPI` + `gateway.gatewayRef` and verify only per-app HTTPRoutes are created (no Gateway) +8. Manual: delete a CR with managed Gateway and verify the Gateway and Ingress are cleaned up +9. Manual: apply a CR with no `networking` field and verify existing NodePort behavior is preserved diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 00000000..1c7d7f17 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,61 @@ +# Releasing Operator v2 + +Production v2 releases are prepared through a reviewed pull request and +published from an annotated `v2.x.y` tag. A single workflow publishes the +operator image and Helm chart from the same commit. + +## Production release + +1. Open a release pull request against `main`. +2. Update `wandb.version` in `deploy/operator/values.yaml` to the intended W&B + server release. +3. Set all three operator versions to the release number without a leading + `v`: + - `version` in `deploy/operator/Chart.yaml` + - `appVersion` in `deploy/operator/Chart.yaml` + - `wandb-operator.image.tag` in `deploy/operator/values.yaml` +4. Run the chart validation commands used by CI: + + ```bash + helm dependency build deploy/operator + ct lint --all --config deploy/ct.yaml + helm lint --strict deploy/operator + ``` + +5. Merge the pull request after all required checks and approvals pass. +6. Update the local branch and confirm it exactly matches the remote branch: + + ```bash + git switch main + git pull --ff-only origin main + test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" + ``` + +7. Create and push an annotated release tag at that commit: + + ```bash + version=v2.0.0 + git tag -a "${version}" -m "Operator ${version}" + git push origin "${version}" + ``` + +8. Monitor the `Release v2` workflow. It publishes the versioned GAR image + first, then the matching OCI Helm chart, and creates the GitHub Release only + after both artifacts succeed. +9. Record the source commit, image digest, chart digest, and GitHub Release URL + in the release record. + +Production versions are immutable. Never move, delete, reuse, or overwrite a +`v2.x.y` tag or its `2.x.y` image/chart tags. If a release is incorrect or only +partially publishes, fix it with a new patch version. The production workflow +does not publish a `latest` tag. + +## Development artifacts + +The `Internal Image Publish` workflow accepts only tags in the form +`dev-<name>-<7-to-40-character-sha>`, for example +`dev-bucket-proxy-1106901`. It cannot publish production-style tags. + +The `Internal Chart Publish` workflow accepts the prerelease version already +declared in `deploy/operator/Chart.yaml`, such as `2.0.0-rc.1`. It rejects +stable versions and refuses to overwrite an existing prerelease chart tag. diff --git a/ginko.mk b/ginko.mk deleted file mode 100644 index 2d81e804..00000000 --- a/ginko.mk +++ /dev/null @@ -1,53 +0,0 @@ -.PHONY: test-ginko -test-ginko: manifests generate fmt vet envtest ginkgo ## Run tests. - @echo "Running tests..." - @KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" \ - ginkgo -p -r --compilers=4 --timeout=5m --fail-fast --race --trace --randomize-all \ - --skip-package e2e \ - --output-interceptor-mode=none \ - --no-color=false \ - --show-node-events \ - --json-report=report.json \ - --junit-report=junit.xml || exit 1 - @echo "All tests passed!" - -.PHONY: test-verbose -test-verbose: manifests generate fmt vet envtest ginkgo ## Run tests with verbose output. - @echo "Running tests in verbose mode..." - @KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" \ - ginkgo -p -r -v --compilers=4 --timeout=5m --fail-fast --race --trace --randomize-all \ - --skip-package e2e \ - --output-interceptor-mode=none \ - --no-color=false \ - --show-node-events \ - --json-report=report.json \ - --junit-report=junit.xml || exit 1 - @echo "All tests passed!" - -.PHONY: test-watch -test-watch: manifests generate fmt vet envtest ginkgo ## Run tests in watch mode. - @echo "Running tests in watch mode..." - @KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" \ - ginkgo watch -p -r --compilers=4 --timeout=5m --fail-fast --race --trace --randomize-all \ - --skip-package e2e \ - --output-interceptor-mode=none \ - --no-color=false \ - --show-node-events \ - --json-report=report.json \ - --junit-report=junit.xml || exit 1 - -.PHONY: test-coverage -test-coverage: manifests generate fmt vet envtest ginkgo ## Run tests with coverage report. - @echo "Running tests with coverage..." - @KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" \ - ginkgo -p -r --compilers=4 --timeout=5m --fail-fast --race --trace --randomize-all \ - --skip-package e2e \ - --output-interceptor-mode=none \ - --no-color=false \ - --show-node-events \ - --json-report=report.json \ - --junit-report=junit.xml \ - --coverprofile=coverage.out || exit 1 - @echo "Generating coverage report..." - @go tool cover -html=coverage.out -o coverage.html - @echo "Coverage report generated at coverage.html" diff --git a/go.mod b/go.mod index 631bdc9f..71bab55d 100644 --- a/go.mod +++ b/go.mod @@ -1,93 +1,133 @@ module github.com/wandb/operator -go 1.25.3 +go 1.26.3 -godebug default=go1.25 +godebug default=go1.26 require ( + github.com/GoogleCloudPlatform/gke-gateway-api v1.4.0 + github.com/Masterminds/goutils v1.1.1 + github.com/Masterminds/semver/v3 v3.4.0 + github.com/cybozu-go/moco v0.34.0 github.com/go-logr/logr v1.4.3 - github.com/go-playground/validator/v10 v10.30.2 + github.com/go-playground/validator/v10 v10.28.0 + github.com/golang/glog v1.2.5 + github.com/google/uuid v1.6.0 + github.com/imdario/mergo v0.3.16 + github.com/kedacore/keda/v2 v2.18.3 + github.com/lmittmann/tint v1.1.2 github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 + github.com/nginx/nginx-gateway-fabric v1.6.2 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 + github.com/opencontainers/image-spec v1.1.1 github.com/pkg/errors v0.9.1 - helm.sh/helm/v4 v4.1.4 - k8s.io/api v0.35.1 - k8s.io/apimachinery v0.35.1 - k8s.io/client-go v0.35.1 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/client_model v0.6.2 + github.com/samber/lo v1.52.0 + github.com/sanity-io/litter v1.3.0 + github.com/stretchr/testify v1.11.1 + github.com/twmb/franz-go v1.21.3 + github.com/twmb/franz-go/pkg/kadm v1.18.0 + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 + golang.org/x/text v0.37.0 + gopkg.in/d4l3k/messagediff.v1 v1.2.1 + gopkg.in/yaml.v3 v3.0.1 + helm.sh/helm/v3 v3.19.2 + k8s.io/api v0.35.3 + k8s.io/apiextensions-apiserver v0.35.3 + k8s.io/apimachinery v0.35.3 + k8s.io/client-go v0.35.3 + k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 + knative.dev/pkg v0.0.0-20250326102644-9f3e60a9244c oras.land/oras-go/v2 v2.6.0 - sigs.k8s.io/controller-runtime v0.23.3 + sigs.k8s.io/controller-runtime v0.22.4 + sigs.k8s.io/gateway-api v1.5.0 + sigs.k8s.io/yaml v1.6.0 ) require ( cel.dev/expr v0.25.1 // indirect - dario.cat/mergo v1.0.1 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect - github.com/ProtonMail/go-crypto v1.3.0 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/cloudflare/circl v1.6.3 // indirect + github.com/chai2010/gettext-go v1.0.3 // indirect + github.com/containerd/containerd v1.7.29 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/d4l3k/messagediff v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/docker/docker-credential-helpers v0.9.4 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/extism/go-sdk v1.7.1 // indirect + github.com/expr-lang/expr v1.17.6 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fluxcd/cli-utils v0.37.2-flux.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/fxamacker/cbor/v2 v2.9.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/swag v0.25.5 // indirect + github.com/go-openapi/swag/cmdutils v0.25.5 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/fileutils v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/mangling v0.25.5 // indirect + github.com/go-openapi/swag/netutils v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/gofrs/flock v0.13.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/cel-go v0.26.0 // indirect - github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/huandu/xstrings v1.5.0 // indirect - github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/mailru/easyjson v0.9.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect + github.com/miekg/dns v1.1.65 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -97,70 +137,70 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect - github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.19.2 // indirect - github.com/rubenv/sql-migrate v1.8.1 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/rubenv/sql-migrate v1.8.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/spf13/cast v1.7.0 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/stoewer/go-strcase v1.3.0 // indirect - github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect - github.com/tetratelabs/wazero v1.11.0 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect + github.com/twmb/franz-go/pkg/kmsg v1.13.1 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect + go.opentelemetry.io/otel/log v0.11.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect - go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.52.0 // indirect - golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/term v0.41.0 // indirect - golang.org/x/text v0.35.0 // indirect - golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.44.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.35.1 // indirect - k8s.io/apiserver v0.35.1 // indirect - k8s.io/cli-runtime v0.35.1 // indirect - k8s.io/component-base v0.35.1 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect - k8s.io/kubectl v0.35.1 // indirect - k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + k8s.io/apiserver v0.35.3 // indirect + k8s.io/cli-runtime v0.35.3 // indirect + k8s.io/component-base v0.35.3 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260330154417-16be699c7b31 // indirect + k8s.io/kubectl v0.35.3 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.21.1 // indirect sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) + +replace github.com/imdario/mergo => github.com/sunsingerus/mergo v0.0.0-20230507185449-fc6fffa94450 + +replace go.podman.io/storage => go.podman.io/storage v1.61.1-0.20260203150839-646d7f9c5763 diff --git a/go.sum b/go.sum index e1fbcba0..1c12ef9c 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,19 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/GoogleCloudPlatform/gke-gateway-api v1.4.0 h1:GY5Ni3evpCoZSz7Qk1ZaPlHj2ou4UmwSv1jv0E4M8xA= +github.com/GoogleCloudPlatform/gke-gateway-api v1.4.0/go.mod h1:IFDp1XhE20jjqWG3o2ocYoz33nCH6HC4rJ6Hdag4y1M= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -22,10 +24,8 @@ github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -34,21 +34,34 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= -github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= +github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE= +github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cybozu-go/moco v0.34.0 h1:zGBSHhKT8FuBuXunuVLVTRxRA8s6cuNF/Yt8WfQiUIQ= +github.com/cybozu-go/moco v0.34.0/go.mod h1:/Z4OximSrHWAFja4HVxnCf2I8jl2srMpjsRuQDFranE= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U= +github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= +github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -61,40 +74,36 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= -github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI= +github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE= -github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= -github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= +github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec= +github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluxcd/cli-utils v0.37.2-flux.1 h1:tQ588ghtRN+E+kHq415FddfqA9v4brn/1WWgrP6rQR0= -github.com/fluxcd/cli-utils v0.37.2-flux.1/go.mod h1:LcWSu1NYET8d8U7O326RhEm5JkQXCMK6ITu4G1CT02c= -github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= -github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= -github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= +github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= @@ -112,45 +121,74 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= +github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= +github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= +github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= +github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= +github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= +github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= +github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM= +github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= +github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= -github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= -github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= -github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= -github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= @@ -161,28 +199,32 @@ github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= -github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= -github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b h1:ogbOPx86mIhFy764gGkqnkFC8m5PJA7sPzlk9ppLVQA= -github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kedacore/keda/v2 v2.18.3 h1:PY3o80tzBAzCffS8J6eBZctT4B+g/r26zErNSMleC50= +github.com/kedacore/keda/v2 v2.18.3/go.mod h1:gaFzDtqtXg6KPmMcwzeCjv7aoK8Z1I2NaUkqHkREbDk= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -199,25 +241,25 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= +github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 h1:yVCLo4+ACVroOEr4iFU1iH46Ldlzz2rTuu18Ra7M8sU= github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2/go.mod h1:VzB2VoMh1Y32/QqDfg9ZJYHj99oM4LiGtqPZydTiQSQ= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= -github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= -github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc= +github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -236,6 +278,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nginx/nginx-gateway-fabric v1.6.2 h1:ktdShWxT/Drh/5/u8S5QMRgnnBGvVuFDV10ttRk0W+c= +github.com/nginx/nginx-gateway-fabric v1.6.2/go.mod h1:Fi2hdmoNj9nQRX9YQDju+ntMPG4Fgcw+irfl/GYWSEk= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= @@ -246,8 +290,13 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -259,22 +308,29 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= -github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= -github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= -github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= +github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0= -github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= +github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= +github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/sanity-io/litter v1.3.0 h1:5ZO+weUsqdSWMUng5JnpkW/Oz8iTXiIdeumhQr1sSjs= +github.com/sanity-io/litter v1.3.0/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= @@ -283,22 +339,23 @@ github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= -github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= -github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -308,10 +365,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= -github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= -github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= -github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +github.com/sunsingerus/mergo v0.0.0-20230507185449-fc6fffa94450 h1:PvdDV9N8PrsoL3ToXH6bId0/OPyt/ExMziouOeyEhco= +github.com/sunsingerus/mergo v0.0.0-20230507185449-fc6fffa94450/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -320,99 +375,104 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/twmb/franz-go v1.21.3 h1:q9Mo8ri+OwBQBjKqrerNCqNWJlJnUDe2qnYsj2V3hdI= +github.com/twmb/franz-go v1.21.3/go.mod h1:rfoMTnVk7107fhTGxfEKIHP/e7tPe6oyij/ywzO0czk= +github.com/twmb/franz-go/pkg/kadm v1.18.0 h1:WRf/LZmDdcDXwX7WMbtDU++v+b3NzYh2bCGoPMmzirw= +github.com/twmb/franz-go/pkg/kadm v1.18.0/go.mod h1:XeLhGoLXLFzK8/ryv5FfpxPxGwj4oFEGpPJMB/x6KDE= +github.com/twmb/franz-go/pkg/kmsg v1.13.1 h1:fG5kItwysTk5UXqVwb64EpQEy3TydF3vYYK21nUQ+bI= +github.com/twmb/franz-go/pkg/kmsg v1.13.1/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI= -go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac= -go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g= -go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= -go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo= -go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= -go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4= -go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= +go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= +go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI= -go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4= +go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= +go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= -go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= -google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI= +google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= @@ -420,6 +480,8 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/d4l3k/messagediff.v1 v1.2.1 h1:70AthpjunwzUiarMHyED52mj9UwtAnE89l1Gmrt3EU0= +gopkg.in/d4l3k/messagediff.v1 v1.2.1/go.mod h1:EUzikiKadqXWcD1AzJLagx0j/BeeWGtn++04Xniyg44= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= @@ -429,36 +491,40 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -helm.sh/helm/v4 v4.1.4 h1:zwTrNkalG4f7SYigRSdQnYrTj0QEz1qzetzAlYoDVSo= -helm.sh/helm/v4 v4.1.4/go.mod h1:5dSo8rRgn3OTkDAc/k0Ipw5/Q+BlqKIKZwa0XwSiINI= -k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= -k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= -k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= -k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= -k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= -k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= -k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs= -k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4= -k8s.io/cli-runtime v0.35.1 h1:uKcXFe8J7AMAM4Gm2JDK4mp198dBEq2nyeYtO+JfGJE= -k8s.io/cli-runtime v0.35.1/go.mod h1:55/hiXIq1C8qIJ3WBrWxEwDLdHQYhBNRdZOz9f7yvTw= -k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= -k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= -k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE= -k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/kubectl v0.35.1 h1:zP3Er8C5i1dcAFUMh9Eva0kVvZHptXIn/+8NtRWMxwg= -k8s.io/kubectl v0.35.1/go.mod h1:cQ2uAPs5IO/kx8R5s5J3Ihv3VCYwrx0obCXum0CvnXo= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +helm.sh/helm/v3 v3.19.2 h1:psQjaM8aIWrSVEly6PgYtLu/y6MRSmok4ERiGhZmtUY= +helm.sh/helm/v3 v3.19.2/go.mod h1:gX10tB5ErM+8fr7bglUUS/UfTOO8UUTYWIBH1IYNnpE= +k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= +k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= +k8s.io/apiextensions-apiserver v0.35.3 h1:2fQUhEO7P17sijylbdwt0nBdXP0TvHrHj0KeqHD8FiU= +k8s.io/apiextensions-apiserver v0.35.3/go.mod h1:tK4Kz58ykRpwAEkXUb634HD1ZAegEElktz/B3jgETd8= +k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= +k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.3 h1:D2eIcfJ05hEAEewoSDg+05e0aSRwx8Y4Agvd/wiomUI= +k8s.io/apiserver v0.35.3/go.mod h1:JI0n9bHYzSgIxgIrfe21dbduJ9NHzKJ6RchcsmIKWKY= +k8s.io/cli-runtime v0.35.3 h1:UZq4ipNimtzBmhN7PPKbfAdqo8quK0H0UdGl6qAQnqI= +k8s.io/cli-runtime v0.35.3/go.mod h1:O7MUmCqcKSd5xI+O5X7/pRkB5l0O2NIhOdUVwbHLXu4= +k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= +k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= +k8s.io/component-base v0.35.3 h1:mbKbzoIMy7JDWS/wqZobYW1JDVRn/RKRaoMQHP9c4P0= +k8s.io/component-base v0.35.3/go.mod h1:IZ8LEG30kPN4Et5NeC7vjNv5aU73ku5MS15iZyvyMYk= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260330154417-16be699c7b31 h1:V+sn9a/1fEYDGwnllCmqXBk8x7obZ+hl869Q3Abumkg= +k8s.io/kube-openapi v0.0.0-20260330154417-16be699c7b31/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/kubectl v0.35.3 h1:1KqSYXk/sodU7VeDvK6atX2kAGUZd2QTeR5K7Hb9r9w= +k8s.io/kubectl v0.35.3/go.mod h1:GPHxZqRe+u/i3gTBoVQHeIyq2NilfNPj9hDWeuN3x5s= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +knative.dev/pkg v0.0.0-20250326102644-9f3e60a9244c h1:6IZwH1QHGfWlmfdy7svgDCPhRqWpisWK/Gcp8wdAwE0= +knative.dev/pkg v0.0.0-20250326102644-9f3e60a9244c/go.mod h1:gx7Pp9NPcKYApNhR8m0KSOeg71pqhwPWhuhUJ6xCa2g= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= -sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= +sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= +sigs.k8s.io/gateway-api v1.5.0 h1:duoo14Ky/fJXpjpmyMISE2RTBGnfCg8zICfTYLTnBJA= +sigs.k8s.io/gateway-api v1.5.0/go.mod h1:GvCETiaMAlLym5CovLxGjS0NysqFk3+Yuq3/rh6QL2o= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= @@ -467,7 +533,7 @@ sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7 sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/golangci.yaml b/golangci.yaml deleted file mode 100644 index 6bbd605f..00000000 --- a/golangci.yaml +++ /dev/null @@ -1,3 +0,0 @@ -linters: - enable: - - errcheck diff --git a/hack/scripts/kind-images-manager.sh b/hack/scripts/kind-images-manager.sh new file mode 100755 index 00000000..682a0620 --- /dev/null +++ b/hack/scripts/kind-images-manager.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGES_FILE=".k8s-images" + +cmd_scrape() { + kubectl get pods -A \ + -o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{range .spec.initContainers[*]}{.image}{"\n"}{end}{end}' \ + | sort -u \ + > "$IMAGES_FILE" + echo "Wrote $(wc -l < "$IMAGES_FILE") images to $IMAGES_FILE" +} + +cmd_load() { + if [[ ! -f "$IMAGES_FILE" ]]; then + echo "error: $IMAGES_FILE not found — run 'scrape' first" >&2 + exit 1 + fi + + local context + context=$(kubectl config current-context) + + if [[ "$context" != kind-* ]]; then + echo "error: current kube context '$context' does not start with 'kind-'" >&2 + exit 1 + fi + + local cluster="${context#kind-}" + + local failed=() + + echo "Loading images into kind cluster '$cluster'..." + while IFS= read -r image; do + [[ -z "$image" ]] && continue + echo " $image" + if ! kind load docker-image "$image" --name "$cluster"; then + echo " warning: failed to load $image" >&2 + failed+=("$image") + fi + done < "$IMAGES_FILE" + + if [[ ${#failed[@]} -gt 0 ]]; then + echo "" >&2 + echo "Failed to load ${#failed[@]} image(s):" >&2 + for image in "${failed[@]}"; do + echo " $image" >&2 + done + exit 1 + fi + + echo "Done." +} + +cmd_pull() { + if [[ ! -f "$IMAGES_FILE" ]]; then + echo "error: $IMAGES_FILE not found — run 'scrape' first" >&2 + exit 1 + fi + + local failed=() + + echo "Pulling images..." + while IFS= read -r image; do + [[ -z "$image" ]] && continue + echo " $image" + if ! docker pull "$image"; then + echo " warning: failed to pull $image" >&2 + failed+=("$image") + fi + done < "$IMAGES_FILE" + + if [[ ${#failed[@]} -gt 0 ]]; then + echo "" >&2 + echo "Failed to pull ${#failed[@]} image(s):" >&2 + for image in "${failed[@]}"; do + echo " $image" >&2 + done + exit 1 + fi + + echo "Done." +} + +case "${1:-}" in + scrape) cmd_scrape ;; + pull) cmd_pull ;; + load) cmd_load ;; + *) + echo "Usage: $(basename "$0") <scrape|pull|load>" >&2 + echo "" + echo "Keep docker images cached on the host and loads them into Kind." + echo "It uses the k8s current context, if named 'kind-*', as the Kind cluster." + echo "This is helpful if you run into rate-limiting problem with a repository." + echo "" + echo "scrape: copies image names from kind cluster into .k8s-images file" + echo "pull: pulls images named .k8s-images file into host's docker" + echo "load: loads images named .k8s-images from host's docker into Kind cluster" + exit 1 + ;; +esac diff --git a/hack/scripts/list-crs.sh b/hack/scripts/list-crs.sh new file mode 100755 index 00000000..a6336bc5 --- /dev/null +++ b/hack/scripts/list-crs.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v kubectl >/dev/null 2>&1; then + echo "error: kubectl is required but not installed" >&2 + exit 1 +fi + +current_context=$(kubectl config current-context 2>/dev/null || true) +if [[ -z "$current_context" ]]; then + echo "error: unable to determine the current kubectl context" >&2 + exit 1 +fi + +mapfile -t crds < <(kubectl get crd -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}') + +if [[ ${#crds[@]} -eq 0 ]]; then + echo "No CRDs found in context: $current_context" + exit 0 +fi + +echo "Current context: $current_context" +echo "" + +found_any=0 +name_column_width=60 + +for crd in "${crds[@]}"; do + [[ -z "$crd" ]] && continue + + group=$(kubectl get crd "$crd" -o jsonpath='{.spec.group}') + plural=$(kubectl get crd "$crd" -o jsonpath='{.spec.names.plural}') + kind=$(kubectl get crd "$crd" -o jsonpath='{.spec.names.kind}') + scope=$(kubectl get crd "$crd" -o jsonpath='{.spec.scope}') + version=$(kubectl get crd "$crd" -o jsonpath='{range .spec.versions[?(@.storage==true)]}{.name}{end}') + + if [[ -z "$version" ]]; then + version=$(kubectl get crd "$crd" -o jsonpath='{.spec.version}') + fi + + resource="${plural}.${group}" + + if [[ "$scope" == "Namespaced" ]]; then + raw_output=$( + kubectl get "$resource" -A \ + -o custom-columns='NAME:.metadata.name,NAMESPACE:.metadata.namespace' \ + --no-headers 2>/dev/null || true + ) + else + raw_output=$( + kubectl get "$resource" \ + -o custom-columns='NAME:.metadata.name' \ + --no-headers 2>/dev/null || true + ) + fi + + [[ -z "$raw_output" ]] && continue + + found_any=1 + printf '\033[1m%s\033[0m\n' "$crd ($kind, ${group}/${version})" + printf '%-*s %s\n' "$name_column_width" "NAME" "SCOPE" + printf '%-*s %s\n' "$name_column_width" "----" "-----" + + while IFS= read -r line; do + [[ -z "$line" ]] && continue + + if [[ "$scope" == "Namespaced" ]]; then + name=${line%%[[:space:]]*} + namespace=${line##*[[:space:]]} + printf '%-*s %s\n' "$name_column_width" "$name" "namespaced:$namespace" + else + printf '%-*s %s\n' "$name_column_width" "$line" "clustered" + fi + done <<< "$raw_output" + + echo "" +done + +if [[ "$found_any" -eq 0 ]]; then + echo "No custom resources found for any CRD in context: $current_context" +fi diff --git a/hack/scripts/managed-connections-to-external.sh b/hack/scripts/managed-connections-to-external.sh new file mode 100755 index 00000000..c6efaa8a --- /dev/null +++ b/hack/scripts/managed-connections-to-external.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v kubectl >/dev/null 2>&1; then + echo "error: kubectl is required but not installed" >&2 + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "error: jq is required but not installed" >&2 + exit 1 +fi + +APP_PREFIX="${1:-wandb}" +NAMESPACE="${2:-default}" +SCRIPT_NAME="$(basename "$0")" + +log() { + printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" +} + +secrets="$(kubectl get secrets -n "${NAMESPACE}" -o json | + jq -r --arg prefix "${APP_PREFIX}" ' + .items[] + | select(.metadata.name | test("^" + $prefix + "-.*-connection$")) + | .metadata.name + ')" + +if [[ -z "${secrets}" ]]; then + log "No secrets matching '${APP_PREFIX}-*-connection' found in namespace '${NAMESPACE}'" + exit 0 +fi + +failed=() + +while IFS= read -r secret_name; do + [[ -z "${secret_name}" ]] && continue + + suffix="${secret_name#"${APP_PREFIX}-"}" + new_name="external-${suffix}" + + log "Copying '${secret_name}' -> '${new_name}'" + + kubectl get secret "${secret_name}" -n "${NAMESPACE}" -o json | + jq --arg name "${new_name}" ' + .metadata.name = $name + | del(.metadata.uid, .metadata.resourceVersion, .metadata.creationTimestamp, + .metadata.ownerReferences, .metadata.managedFields, .metadata.annotations["kubectl.kubernetes.io/last-applied-configuration"]) + ' | + kubectl apply -n "${NAMESPACE}" -f - || { + log "Failed to create '${new_name}', skipping deletion of '${secret_name}'" + failed+=("${secret_name}") + continue + } + + log "Deleting '${secret_name}'" + kubectl delete secret "${secret_name}" -n "${NAMESPACE}" +done <<< "${secrets}" + +if [[ "${#failed[@]}" -gt 0 ]]; then + log "Completed with errors. Failed to migrate: ${failed[*]}" + exit 1 +fi + +log "Done" diff --git a/hack/scripts/pull-server-manifest.sh b/hack/scripts/pull-server-manifest.sh new file mode 100755 index 00000000..acec9b15 --- /dev/null +++ b/hack/scripts/pull-server-manifest.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPOSITORY="us-docker.pkg.dev/wandb-production/public/wandb/server-manifest" +SCRIPT_NAME="$(basename "$0")" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +DEST_ROOT="${REPO_ROOT}/hack/testing-manifests/server-manifest" + +log() { + printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" +} + +usage() { + cat <<EOF +Usage: ${SCRIPT_NAME} <tag> + +Downloads the published server manifest OCI artifact for <tag> from +${REPOSITORY} and unpacks its manifest yaml files into +${DEST_ROOT}/<tag>, replacing any existing manifest already checked in +for that version. +EOF +} + +if ! command -v oras >/dev/null 2>&1; then + echo "error: oras is required but not installed (https://oras.land/docs/installation)" >&2 + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "error: jq is required but not installed" >&2 + exit 1 +fi + +if [[ $# -ne 1 || "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 1 +fi + +TAG="$1" +IMAGE_REF="${REPOSITORY}:${TAG}" +DEST_DIR="${DEST_ROOT}/${TAG}" + +WORKDIR="$(mktemp -d)" +trap 'rm -rf "${WORKDIR}"' EXIT + +log "Fetching manifest for ${IMAGE_REF}" +MANIFEST_JSON="$(oras manifest fetch "${IMAGE_REF}")" + +# Multi-platform tags resolve to an image index; follow the first entry, same +# as pkg/wandb/manifest.processManifest does for the operator itself. +IS_INDEX="$(jq -r 'if .manifests then "true" else "false" end' <<<"${MANIFEST_JSON}")" +if [[ "${IS_INDEX}" == "true" ]]; then + CHILD_DIGEST="$(jq -r '.manifests[0].digest' <<<"${MANIFEST_JSON}")" + log "Tag resolved to an image index; following ${CHILD_DIGEST}" + MANIFEST_JSON="$(oras manifest fetch "${REPOSITORY}@${CHILD_DIGEST}")" +fi + +LAYER_DIGESTS="$(jq -r '.layers[]?.digest' <<<"${MANIFEST_JSON}")" +if [[ -z "${LAYER_DIGESTS}" ]]; then + echo "error: no layers found in manifest for ${IMAGE_REF}" >&2 + exit 1 +fi + +EXTRACT_DIR="${WORKDIR}/extracted" +mkdir -p "${EXTRACT_DIR}" + +while IFS= read -r digest; do + [[ -z "${digest}" ]] && continue + log "Extracting layer ${digest}" + oras blob fetch "${REPOSITORY}@${digest}" --output - | tar -xf - -C "${EXTRACT_DIR}" +done <<<"${LAYER_DIGESTS}" + +YAML_FILES=() +while IFS= read -r f; do + [[ -z "${f}" ]] && continue + YAML_FILES+=("${f}") +done < <(find "${EXTRACT_DIR}" -type f -name '*.yaml' | sort) +if [[ ${#YAML_FILES[@]} -eq 0 ]]; then + echo "error: no .yaml files found in manifest layers for ${IMAGE_REF}" >&2 + exit 1 +fi + +rm -rf "${DEST_DIR}" +mkdir -p "${DEST_DIR}" + +for f in "${YAML_FILES[@]}"; do + name="$(basename "${f}")" + if [[ -e "${DEST_DIR}/${name}" ]]; then + log "warning: duplicate manifest file name '${name}' found across layers; last one wins" + fi + cp "${f}" "${DEST_DIR}/${name}" +done + +log "Wrote ${#YAML_FILES[@]} manifest file(s) to ${DEST_DIR}" diff --git a/hack/scripts/setup_crc.sh b/hack/scripts/setup_crc.sh new file mode 100755 index 00000000..c377f179 --- /dev/null +++ b/hack/scripts/setup_crc.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +set -euo pipefail + +command -v crc >/dev/null 2>&1 || { echo "Error: crc is required but not installed." >&2; echo "Download from: https://console.redhat.com/openshift/create/local" >&2; exit 1; } +command -v oc >/dev/null 2>&1 || { echo "Error: oc is required but not installed." >&2; echo "Install with: brew install openshift-cli" >&2; exit 1; } +command -v docker >/dev/null 2>&1 || { echo "Error: docker is required but not installed." >&2; exit 1; } + +echo "Configuring CRC resources..." +crc config set cpus 6 >/dev/null +crc config set memory 16384 >/dev/null +crc config set disk-size 80 >/dev/null + +CRC_STATUS=$(crc status --output json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('crcStatus','Unknown'))" 2>/dev/null || echo "Unknown") +if [[ "$CRC_STATUS" != "Running" ]]; then + echo "Starting CRC..." + crc start +fi + +eval "$(crc oc-env)" + +KUBEADMIN_PASSWORD=$(crc console --credentials 2>/dev/null | grep kubeadmin | sed "s/.*-p \([^ ]*\) .*/\1/" || true) +if [[ -z "$KUBEADMIN_PASSWORD" ]]; then + echo "Error: could not retrieve kubeadmin password from CRC." + exit 1 +fi + +echo "Logging in as kubeadmin..." +oc login -u kubeadmin -p "$KUBEADMIN_PASSWORD" https://api.crc.testing:6443 + +echo "Exposing the internal image registry..." +oc patch configs.imageregistry.operator.openshift.io/cluster \ + --patch '{"spec":{"defaultRoute":true}}' --type=merge + +REGISTRY="default-route-openshift-image-registry.apps-crc.testing" +echo "Waiting for registry route..." +for i in $(seq 1 30); do + if curl -sk "https://$REGISTRY/healthz" >/dev/null 2>&1; then + break + fi + if [[ $i -eq 30 ]]; then + echo "Error: registry route not ready after 30s." + exit 1 + fi + sleep 1 +done + +echo "Logging Docker into the CRC registry..." +docker login -u kubeadmin -p "$(oc whoami -t)" "$REGISTRY" + +ORBSTACK_DOCKER_CONFIG="$HOME/.orbstack/config/docker.json" +if docker info 2>&1 | grep -q "Operating System: OrbStack"; then + if ! grep -q "$REGISTRY" "$ORBSTACK_DOCKER_CONFIG" 2>/dev/null; then + echo "Configuring OrbStack Docker to trust the CRC registry..." + mkdir -p "$(dirname "$ORBSTACK_DOCKER_CONFIG")" + python3 -c " +import json, pathlib, sys +p = pathlib.Path('$ORBSTACK_DOCKER_CONFIG') +cfg = json.loads(p.read_text()) if p.exists() else {} +regs = set(cfg.get('insecure-registries', [])) +regs.add('$REGISTRY') +cfg['insecure-registries'] = sorted(regs) +p.write_text(json.dumps(cfg, indent=2) + '\n') +" + echo "Restarting OrbStack Docker daemon..." + orb restart docker 2>/dev/null || true + sleep 3 + docker login -u kubeadmin -p "$(oc whoami -t)" "$REGISTRY" + fi +fi + +echo "Creating wandb-operators namespace..." +oc new-project wandb-operators 2>/dev/null || oc project wandb-operators 2>/dev/null || true + +echo "Creating wandb namespace..." +oc new-project wandb 2>/dev/null || oc project wandb 2>/dev/null || true + +kubectl config use-context crc-admin + +echo "" +echo "Done. CRC is ready for Tilt." +echo "" +echo "Configure tilt-settings.star:" +echo ' SETTINGS = {' +echo ' "allowedContexts": ["crc-admin"],' +echo ' "openshiftSCC": True,' +echo ' }' +echo "" +echo "Then run: tilt up" diff --git a/hack/scripts/setup_kind.sh b/hack/scripts/setup_kind.sh new file mode 100755 index 00000000..772c46b3 --- /dev/null +++ b/hack/scripts/setup_kind.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +set -e + +WORKER_NODES="${1:-0}" + +if ! [[ "$WORKER_NODES" =~ ^[0-9]+$ ]]; then + echo "Error: WORKER_NODES must be a non-negative integer, got '$WORKER_NODES'" + exit 1 +fi + +CLUSTER_NAME="kind" + +read_star_setting() { + local key="$1" + local file="tilt-settings.star" + if [[ -f "$file" ]]; then + grep "\"$key\"" "$file" | sed 's/.*: *"\(.*\)".*/\1/' | head -1 + fi +} + +KIND_CLUSTER_NAME=$(read_star_setting "kindClusterName") +if [[ -n "$KIND_CLUSTER_NAME" ]]; then + CLUSTER_NAME="$KIND_CLUSTER_NAME" +fi + +echo "Creating kind cluster: $CLUSTER_NAME" + +if kind get clusters | grep -q "^$CLUSTER_NAME$"; then + echo "Kind cluster '$CLUSTER_NAME' already exists" + kubectl config use-context "kind-$CLUSTER_NAME" + exit 0 +fi + +KIND_CONFIG="kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4" + +if [[ "$WORKER_NODES" -gt 0 ]]; then + KIND_CONFIG+=$'\nnodes:\n- role: control-plane' + for ((i = 0; i < WORKER_NODES; i++)); do + KIND_CONFIG+=$'\n- role: worker' + done +fi + +echo "$KIND_CONFIG" | kind create cluster --name "$CLUSTER_NAME" --config=- + +echo "Kind cluster '$CLUSTER_NAME' created successfully with $WORKER_NODES worker nodes" + +echo "Installing Kubernetes Metrics Server..." +kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml +kubectl patch -n kube-system deployment metrics-server --type=json \ + -p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]' diff --git a/hack/scripts/teardown_kind.sh b/hack/scripts/teardown_kind.sh new file mode 100755 index 00000000..cffb451d --- /dev/null +++ b/hack/scripts/teardown_kind.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -e + +CLUSTER_NAME="kind" + +read_star_setting() { + local key="$1" + local file="tilt-settings.star" + if [[ -f "$file" ]]; then + grep "\"$key\"" "$file" | sed 's/.*: *"\(.*\)".*/\1/' | head -1 + fi +} + +KIND_CLUSTER_NAME=$(read_star_setting "kindClusterName") +if [[ -n "$KIND_CLUSTER_NAME" ]]; then + CLUSTER_NAME="$KIND_CLUSTER_NAME" +fi + +echo "Deleting kind cluster: $CLUSTER_NAME" + +if ! kind get clusters | grep -q "^$CLUSTER_NAME$"; then + echo "Kind cluster '$CLUSTER_NAME' does not exist" + exit 0 +fi + +kind delete cluster --name "$CLUSTER_NAME" + +echo "Kind cluster '$CLUSTER_NAME' deleted successfully" + +if [[ -d "dist" ]]; then + echo "Cleaning dist/ directory..." + rm -rf dist + echo "dist/ directory removed" +fi diff --git a/hack/scripts/test-weave.py b/hack/scripts/test-weave.py new file mode 100755 index 00000000..f4b2c0aa --- /dev/null +++ b/hack/scripts/test-weave.py @@ -0,0 +1,64 @@ +import weave +import json +from datetime import datetime + +# Initialize weave +weave.init('mock-trace-example') + +# Define a mock LLM call function +@weave.op() +def mock_llm_call(prompt: str) -> str: + # Instead of making an actual LLM call, we'll just return a mock response + mock_response = { + "id": "mock-llm-call-123", + "model": "gpt-4", + "created": datetime.now().isoformat(), + "choices": [{ + "message": { + "role": "assistant", + "content": "This is a mock response to: " + prompt + } + }] + } + + return json.dumps(mock_response) + +# Create a mock trace +@weave.op() +def create_mock_trace(): + # Call the mock LLM function + response = mock_llm_call("What is the capital of France?") + + # Return the response + return { + "prompt": "What is the capital of France?", + "response": response, + "metadata": { + "model": "gpt-4", + "temperature": 0.7, + "max_tokens": 100 + } + } + +if __name__ == "__main__": + # Create and log the mock trace + result, call = create_mock_trace.call() + + # Set a custom display name for the call + call.set_display_name("Mock LLM Call - Capital of France") + + # Add feedback to the call using the correct method + call.feedback.add("rating", {"value": 5, "comment": "Great response! Very informative."}) + + # Get call information and convert datetime objects to ISO format strings + call_info = { + "id": call.id, + "trace_id": call.trace_id, + "started_at": call.started_at.isoformat() if call.started_at else None, + "ended_at": call.ended_at.isoformat() if call.ended_at else None, + "inputs": call.inputs, + "output": call.output + } + +print("Mock trace created and logged successfully!") +print("Call information:", json.dumps(call_info, indent=2)) \ No newline at end of file diff --git a/hack/scripts/tilt-dev-clean.sh b/hack/scripts/tilt-dev-clean.sh new file mode 100755 index 00000000..c10d127e --- /dev/null +++ b/hack/scripts/tilt-dev-clean.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_NAME="$(basename "$0")" +OPERATOR_NAMESPACE="${OPERATOR_NAMESPACE:-wandb-operators}" +LEGACY_OPERATOR_NAMESPACE="wandb-operator" +WAIT_TIMEOUT="10m" +DRY_RUN="false" +APP_NAMESPACE="" +APP_NAME="" + +usage() { + cat <<EOF +Usage: ${SCRIPT_NAME} [--namespace <ns>] [--name <wandb-name>] [--dry-run] + +Safely cleans up dev W&B installs before a fresh Tilt rebuild: +1. Deletes the W&B CR while the operator is still running +2. Waits for finalizer-driven cleanup to complete +3. Uninstalls Tilt-managed Helm releases +4. Deletes dev-only PVCs and generated secrets for the app + +By default, it targets all WeightsAndBiases resources labeled: + app.kubernetes.io/managed-by=tilt + +Options: + --namespace <ns> Limit cleanup to a single namespace + --name <name> Limit cleanup to a single WeightsAndBiases resource + --dry-run Print actions without executing them + -h, --help Show this help text +EOF +} + +log() { + printf '[%s] %s\n' "${SCRIPT_NAME}" "$*" +} + +run() { + if [[ "${DRY_RUN}" == "true" ]]; then + printf '[dry-run] %s\n' "$*" + return 0 + fi + + "$@" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --namespace) + APP_NAMESPACE="${2:?missing namespace value}" + shift 2 + ;; + --name) + APP_NAME="${2:?missing name value}" + shift 2 + ;; + --dry-run) + DRY_RUN="true" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ -n "${APP_NAME}" && -z "${APP_NAMESPACE}" ]]; then + echo "--name requires --namespace" >&2 + exit 1 +fi + +discover_apps() { + if [[ -n "${APP_NAME}" ]]; then + kubectl get weightsandbiases.apps.wandb.com "${APP_NAME}" -n "${APP_NAMESPACE}" -o json | + jq -r '[.metadata.namespace, .metadata.name] | @tsv' + return + fi + + kubectl get weightsandbiases.apps.wandb.com -A -o json | + jq -r ' + .items[] + | select(.metadata.labels["app.kubernetes.io/managed-by"] == "tilt") + | [.metadata.namespace, .metadata.name] + | @tsv + ' +} + +delete_app_and_wait() { + local namespace="$1" + local name="$2" + + log "Deleting WeightsAndBiases ${namespace}/${name}" + run kubectl delete weightsandbiases.apps.wandb.com "${name}" -n "${namespace}" --ignore-not-found + + if [[ "${DRY_RUN}" == "true" ]]; then + printf '[dry-run] kubectl wait --for=delete weightsandbiases.apps.wandb.com/%s -n %s --timeout=%s\n' "${name}" "${namespace}" "${WAIT_TIMEOUT}" + return 0 + fi + + if ! kubectl wait --for=delete "weightsandbiases.apps.wandb.com/${name}" -n "${namespace}" --timeout="${WAIT_TIMEOUT}"; then + log "Timed out waiting for ${namespace}/${name} to finish finalizer cleanup" + kubectl get weightsandbiases.apps.wandb.com "${name}" -n "${namespace}" -o yaml || true + exit 1 + fi +} + +delete_state_for_app() { + local namespace="$1" + local name="$2" + + log "Deleting dev PVCs for ${namespace}/${name}" + run kubectl delete pvc -n "${namespace}" -l "weightsandbiases.apps.wandb.com/name=${name}" --ignore-not-found + + log "Deleting labeled dev secrets for ${namespace}/${name}" + run kubectl delete secret -n "${namespace}" -l "weightsandbiases.apps.wandb.com/name=${name}" --ignore-not-found + + local prefixed_secrets + prefixed_secrets="$(kubectl get secret -n "${namespace}" -o json | + jq -r --arg prefix "${name}" ' + .items[] + | select(.metadata.name | startswith($prefix + "-")) + | .metadata.name + ')" + + if [[ -n "${prefixed_secrets}" ]]; then + log "Deleting prefixed dev secrets for ${namespace}/${name}" + while IFS= read -r secret_name; do + [[ -z "${secret_name}" ]] && continue + run kubectl delete secret -n "${namespace}" "${secret_name}" --ignore-not-found + done <<< "${prefixed_secrets}" + fi + + run kubectl delete secret -n "${namespace}" wandb-otel-connection --ignore-not-found +} + +uninstall_release() { + local namespace="$1" + local release_name="$2" + + if helm status "${release_name}" --namespace "${namespace}" >/dev/null 2>&1; then + log "Uninstalling Helm release ${namespace}/${release_name}" + run helm uninstall "${release_name}" --namespace "${namespace}" + else + log "Helm release ${namespace}/${release_name} is already absent" + fi +} + +apps=() +while IFS= read -r app; do + [[ -z "${app}" ]] && continue + apps+=("${app}") +done < <(discover_apps) + +if [[ "${#apps[@]}" -eq 0 ]]; then + log "No Tilt-managed WeightsAndBiases resources found" +else + log "Found ${#apps[@]} Tilt-managed WeightsAndBiases resource(s)" +fi + +for app in "${apps[@]}"; do + namespace="${app%%$'\t'*}" + name="${app##*$'\t'}" + delete_app_and_wait "${namespace}" "${name}" +done + +uninstall_release "${OPERATOR_NAMESPACE}" "wandb-operator" +uninstall_release "${LEGACY_OPERATOR_NAMESPACE}" "telemetry-stack" +uninstall_release "${LEGACY_OPERATOR_NAMESPACE}" "third-party-operators" + +for app in "${apps[@]}"; do + namespace="${app%%$'\t'*}" + name="${app##*$'\t'}" + delete_state_for_app "${namespace}" "${name}" +done + +log "Dev cleanup complete" diff --git a/hack/scripts/tilt-down-dev-clean.sh b/hack/scripts/tilt-down-dev-clean.sh new file mode 100755 index 00000000..9f7e1553 --- /dev/null +++ b/hack/scripts/tilt-down-dev-clean.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + cat <<EOF +Usage: $(basename "$0") [tilt-dev-clean args] + +Runs the safe dev cleanup flow, then calls \`tilt down\`. + +Examples: + $(basename "$0") + $(basename "$0") --namespace default --name wandb-dev-v2 + $(basename "$0") --dry-run +EOF + exit 0 +fi + +"${SCRIPT_DIR}/tilt-dev-clean.sh" "$@" + +if [[ " $* " == *" --dry-run "* ]]; then + exit 0 +fi + +tilt down diff --git a/hack/scripts/verify-custom-ca-e2e.sh b/hack/scripts/verify-custom-ca-e2e.sh new file mode 100755 index 00000000..66819016 --- /dev/null +++ b/hack/scripts/verify-custom-ca-e2e.sh @@ -0,0 +1,303 @@ +#!/usr/bin/env bash + +set -euo pipefail + +NAMESPACE="wandb-ca-e2e" +NAME="wandb" +API_APP="api" +TIMEOUT="20m" +POLL_SECONDS=10 + +usage() { + cat <<EOF +Usage: $(basename "$0") [--namespace <ns>] [--name <wandb-name>] [--api-app <application>] [--timeout <duration>] + +Verifies the Tilt custom CA e2e path: + - inline and user-provided CA ConfigMaps + - generated MySQL/Redis connection URL CA parameters when configured + - W&B workload pod template env, volumes, mounts, and checksum annotation + - live workload pod CA files when a ready workload pod is available + - recent workload logs for TLS trust failures +EOF +} + +log() { + printf '[custom-ca-e2e] %s\n' "$*" +} + +fail() { + printf '[custom-ca-e2e] ERROR: %s\n' "$*" >&2 + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1" +} + +duration_seconds() { + case "$1" in + *m) echo $((${1%m} * 60)) ;; + *s) echo "${1%s}" ;; + *) echo "$1" ;; + esac +} + +wait_until() { + local description="$1" + shift + local deadline + deadline=$(($(date +%s) + $(duration_seconds "${TIMEOUT}"))) + + log "Waiting for ${description}" + until "$@"; do + if [[ "$(date +%s)" -ge "${deadline}" ]]; then + fail "timed out waiting for ${description}" + fi + sleep "${POLL_SECONDS}" + done +} + +secret_value() { + local secret="$1" + local key="$2" + kubectl -n "${NAMESPACE}" get secret "${secret}" -o json | + python3 -c 'import base64,json,sys; obj=json.load(sys.stdin); print(base64.b64decode(obj["data"][sys.argv[1]]).decode())' "${key}" +} + +assert_url_param() { + local url="$1" + local key="$2" + local expected="$3" + python3 - "${url}" "${key}" "${expected}" <<'PY' +import sys +from urllib.parse import parse_qs, urlparse + +url, key, expected = sys.argv[1:] +actual = parse_qs(urlparse(url).query).get(key, [""])[0] +if actual != expected: + raise SystemExit(f"{key}={actual!r}, expected {expected!r} in {url}") +PY +} + +json_has_env() { + local name="$1" + jq -e --arg name "${name}" '[.spec.containers[]?.env[]? | select(.name == $name)] | length > 0' >/dev/null +} + +json_has_volume() { + local name="$1" + jq -e --arg name "${name}" '[.spec.volumes[]? | select(.name == $name)] | length > 0' >/dev/null +} + +json_has_mount() { + local name="$1" + local path="$2" + jq -e --arg name "${name}" --arg path "${path}" \ + '[.spec.containers[]?.volumeMounts[]? | select(.name == $name and .mountPath == $path)] | length > 0' >/dev/null +} + +check_wandb_ready() { + kubectl -n "${NAMESPACE}" get weightsandbiases.apps.wandb.com "${NAME}" -o json | + jq -e '.status.ready == true' >/dev/null +} + +check_application_ready() { + kubectl -n "${NAMESPACE}" get application "${API_APP}" -o json | + jq -e '.status.ready == true' >/dev/null +} + +selected_migration_job_json() { + kubectl -n "${NAMESPACE}" get jobs \ + -l "app.kubernetes.io/component=migration,app.kubernetes.io/instance=${NAME},app.kubernetes.io/managed-by=wandb-operator" \ + -o json | + jq -er ' + [ + .items[] + | select(.spec.template.metadata.annotations["weightsandbiases.apps.wandb.com/ca-certs-checksum"]? != null) + ] + | sort_by(.status.succeeded // 0) + | reverse + | .[0] + ' +} + +check_migration_workload_exists() { + selected_migration_job_json >/dev/null +} + +select_workload_template() { + if kubectl -n "${NAMESPACE}" get application "${API_APP}" >/dev/null 2>&1; then + wait_until "Application ${NAMESPACE}/${API_APP} status.ready=true" check_application_ready + WORKLOAD_KIND="Application" + WORKLOAD_NAME="${API_APP}" + WORKLOAD_POD_SELECTOR="app.kubernetes.io/name=${API_APP},app.kubernetes.io/instance=${NAME}" + WORKLOAD_TEMPLATE_JSON="$(kubectl -n "${NAMESPACE}" get application "${API_APP}" -o json | jq -c '.spec.podTemplate')" + return + fi + + log "Application ${NAMESPACE}/${API_APP} not found; falling back to a custom-CA-injected W&B migration job" + wait_until "custom-CA-injected W&B migration job" check_migration_workload_exists + local job_json + job_json="$(selected_migration_job_json)" + WORKLOAD_KIND="Job" + WORKLOAD_NAME="$(echo "${job_json}" | jq -r '.metadata.name')" + WORKLOAD_POD_SELECTOR="job-name=${WORKLOAD_NAME}" + WORKLOAD_TEMPLATE_JSON="$(echo "${job_json}" | jq -c '.spec.template')" +} + +ready_workload_pod_name() { + kubectl -n "${NAMESPACE}" get pods -l "${WORKLOAD_POD_SELECTOR}" -o json | + jq -r ' + .items[] + | select(.status.phase == "Running") + | select(any(.status.containerStatuses[]?; .ready == true)) + | .metadata.name + ' | + head -n 1 +} + +check_inline_configmap() { + kubectl -n "${NAMESPACE}" get configmap "${NAME}-ca-certs" -o json | + jq -e '.data["customCA0.crt"] | contains("BEGIN CERTIFICATE")' >/dev/null +} + +check_user_configmap() { + kubectl -n "${NAMESPACE}" get configmap "${USER_CONFIGMAP}" -o json | + jq -e '.data | to_entries | any(.value | contains("BEGIN CERTIFICATE"))' >/dev/null +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --namespace) + NAMESPACE="${2:?missing namespace}" + shift 2 + ;; + --name) + NAME="${2:?missing name}" + shift 2 + ;; + --api-app) + API_APP="${2:?missing api app name}" + shift 2 + ;; + --timeout) + TIMEOUT="${2:?missing timeout}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + fail "unknown argument: $1" + ;; + esac +done + +need_cmd kubectl +need_cmd jq +need_cmd python3 + +wait_until "WeightsAndBiases ${NAMESPACE}/${NAME} status.ready=true" check_wandb_ready + +wandb_json="$(kubectl -n "${NAMESPACE}" get weightsandbiases.apps.wandb.com "${NAME}" -o json)" +inline_ca_count="$(echo "${wandb_json}" | jq -r '(.spec.global.customCACerts // []) | length')" +USER_CONFIGMAP="$(echo "${wandb_json}" | jq -r '.spec.global.caCertsConfigMap // ""')" +mysql_ca_enabled="$(echo "${wandb_json}" | jq -r '(((.spec.mysql.externalMysql.sslCa.name // "") | length) > 0 and ((.spec.mysql.externalMysql.sslCa.key // "") | length) > 0)')" +redis_ca_enabled="$(echo "${wandb_json}" | jq -r '(((.spec.redis.externalRedis.sslCa.name // "") | length) > 0 and ((.spec.redis.externalRedis.sslCa.key // "") | length) > 0)')" + +if [[ "${inline_ca_count}" == "0" && -z "${USER_CONFIGMAP}" ]]; then + fail "WeightsAndBiases ${NAMESPACE}/${NAME} does not configure global custom CA material" +fi + +if [[ "${inline_ca_count}" != "0" ]]; then + wait_until "inline custom CA ConfigMap" check_inline_configmap +fi + +if [[ -n "${USER_CONFIGMAP}" ]]; then + wait_until "user custom CA ConfigMap ${USER_CONFIGMAP}" check_user_configmap +fi + +if [[ "${mysql_ca_enabled}" == "true" ]]; then + mysql_url="$(secret_value wandb-mysql-connection url)" + assert_url_param "${mysql_url}" "tls" "custom" + assert_url_param "${mysql_url}" "ssl-ca" "/etc/ssl/certs/mysql_ca.pem" + log "MySQL connection URL includes expected CA parameters" +fi + +if [[ "${redis_ca_enabled}" == "true" ]]; then + redis_url="$(secret_value wandb-redis-connection url)" + assert_url_param "${redis_url}" "tls" "true" + assert_url_param "${redis_url}" "caCertPath" "/etc/ssl/certs/redis_ca.pem" + log "Redis connection URL includes expected CA parameters" +fi + +select_workload_template +workload_ref="${WORKLOAD_KIND} ${WORKLOAD_NAME}" + +for env_name in SSL_CERT_FILE SSL_CERT_DIR REQUESTS_CA_BUNDLE; do + echo "${WORKLOAD_TEMPLATE_JSON}" | json_has_env "${env_name}" || fail "missing env ${env_name} on ${workload_ref}" +done +if [[ "${mysql_ca_enabled}" == "true" ]]; then + echo "${WORKLOAD_TEMPLATE_JSON}" | json_has_env MYSQL_CA_CERT_PATH || fail "missing env MYSQL_CA_CERT_PATH on ${workload_ref}" +fi + +echo "${WORKLOAD_TEMPLATE_JSON}" | json_has_volume wandb-ca-certs-root || fail "missing volume wandb-ca-certs-root on ${workload_ref}" +echo "${WORKLOAD_TEMPLATE_JSON}" | json_has_mount wandb-ca-certs-root /usr/local/share/ca-certificates/ || + fail "missing root CA mount" + +if [[ "${inline_ca_count}" != "0" ]]; then + echo "${WORKLOAD_TEMPLATE_JSON}" | json_has_volume wandb-ca-certs || fail "missing volume wandb-ca-certs on ${workload_ref}" + echo "${WORKLOAD_TEMPLATE_JSON}" | json_has_mount wandb-ca-certs /usr/local/share/ca-certificates/inline || + fail "missing inline CA mount" +fi +if [[ -n "${USER_CONFIGMAP}" ]]; then + echo "${WORKLOAD_TEMPLATE_JSON}" | json_has_volume wandb-ca-certs-user || fail "missing volume wandb-ca-certs-user on ${workload_ref}" + echo "${WORKLOAD_TEMPLATE_JSON}" | json_has_mount wandb-ca-certs-user /usr/local/share/ca-certificates/configmap || + fail "missing user CA ConfigMap mount" +fi +if [[ "${mysql_ca_enabled}" == "true" ]]; then + echo "${WORKLOAD_TEMPLATE_JSON}" | json_has_volume mysql-ca || fail "missing volume mysql-ca on ${workload_ref}" + echo "${WORKLOAD_TEMPLATE_JSON}" | json_has_mount mysql-ca /etc/ssl/certs/mysql_ca.pem || + fail "missing MySQL CA mount" +fi +if [[ "${redis_ca_enabled}" == "true" ]]; then + echo "${WORKLOAD_TEMPLATE_JSON}" | json_has_volume redis-ca || fail "missing volume redis-ca on ${workload_ref}" + echo "${WORKLOAD_TEMPLATE_JSON}" | json_has_mount redis-ca /etc/ssl/certs/redis_ca.pem || + fail "missing Redis CA mount" +fi +echo "${WORKLOAD_TEMPLATE_JSON}" | + jq -e '.metadata.annotations["weightsandbiases.apps.wandb.com/ca-certs-checksum"] | length > 0' >/dev/null || + fail "missing CA checksum annotation" +log "${workload_ref} pod template contains expected CA env, mounts, volumes, and checksum" + +workload_pod="$(ready_workload_pod_name)" +if [[ -n "${workload_pod}" ]]; then + pod_checks=("test -d /usr/local/share/ca-certificates/") + if [[ "${inline_ca_count}" != "0" ]]; then + pod_checks+=("test -d /usr/local/share/ca-certificates/inline") + fi + if [[ -n "${USER_CONFIGMAP}" ]]; then + pod_checks+=("test -d /usr/local/share/ca-certificates/configmap") + fi + if [[ "${mysql_ca_enabled}" == "true" ]]; then + pod_checks+=("test -s /etc/ssl/certs/mysql_ca.pem") + fi + if [[ "${redis_ca_enabled}" == "true" ]]; then + pod_checks+=("test -s /etc/ssl/certs/redis_ca.pem") + fi + pod_check_cmd="$(printf ' && %s' "${pod_checks[@]}")" + kubectl -n "${NAMESPACE}" exec "${workload_pod}" -- sh -c "${pod_check_cmd# && }" || + fail "live ${workload_ref} pod does not have expected CA files" + log "Live ${workload_ref} pod has expected CA files" +else + log "No ready pod found for ${workload_ref}; verified the workload pod template and skipped live filesystem checks" +fi + +workload_logs="$(kubectl -n "${NAMESPACE}" logs -l "${WORKLOAD_POD_SELECTOR}" --all-containers --tail=500 --prefix=true 2>/dev/null || true)" +if echo "${workload_logs}" | grep -Eiq 'x509:|certificate signed by unknown authority|unknown authority|tls: failed to verify'; then + echo "${workload_logs}" >&2 + fail "recent ${workload_ref} logs contain TLS trust failures" +fi + +log "Custom CA e2e verification passed" diff --git a/hack/testing-manifests/cert-manager/ca-certificate.yaml b/hack/testing-manifests/cert-manager/ca-certificate.yaml new file mode 100644 index 00000000..35bfe803 --- /dev/null +++ b/hack/testing-manifests/cert-manager/ca-certificate.yaml @@ -0,0 +1,11 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: ca-certificate +spec: + isCA: true + commonName: wandb-ca + secretName: ca-certificate + issuerRef: + name: selfsigned-issuer + kind: Issuer diff --git a/hack/testing-manifests/cert-manager/ca-issuer.yaml b/hack/testing-manifests/cert-manager/ca-issuer.yaml new file mode 100644 index 00000000..5f53c8a5 --- /dev/null +++ b/hack/testing-manifests/cert-manager/ca-issuer.yaml @@ -0,0 +1,7 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: ca-issuer +spec: + ca: + secretName: ca-certificate diff --git a/hack/testing-manifests/cert-manager/selfsigned-issuer.yaml b/hack/testing-manifests/cert-manager/selfsigned-issuer.yaml new file mode 100644 index 00000000..8d3d6eee --- /dev/null +++ b/hack/testing-manifests/cert-manager/selfsigned-issuer.yaml @@ -0,0 +1,6 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: selfsigned-issuer +spec: + selfSigned: {} diff --git a/hack/testing-manifests/minio/minio.yaml b/hack/testing-manifests/minio/minio.yaml deleted file mode 100644 index 2c8908e2..00000000 --- a/hack/testing-manifests/minio/minio.yaml +++ /dev/null @@ -1,51 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: minio # Change this value if you want a different namespace name - labels: - name: minio # Change this value to match metadata.name ---- -apiVersion: v1 -kind: Pod -metadata: - labels: - app: minio - name: minio - namespace: minio -spec: - containers: - - name: minio - image: quay.io/minio/minio:latest - env: - - name: MINIO_ACCESS_KEY - value: "minio" - - name: MINIO_SECRET_KEY - value: "minio123" - command: - - /bin/bash - - -c - args: - - mkdir -p /data/bucket && minio server /data --console-address :9090 - ports: - - containerPort: 9000 - name: minio - volumeMounts: - - mountPath: /data - name: localvolume # - volumes: - - name: localvolume - hostPath: - path: /mnt/minio/data - type: DirectoryOrCreate ---- -apiVersion: v1 -kind: Service -metadata: - name: minio - namespace: minio # Change this value to match the namespace metadata.name -spec: - selector: - app: minio - ports: - - port: 9000 - name: minio \ No newline at end of file diff --git a/hack/testing-manifests/seaweedfs/seaweedfs.yaml b/hack/testing-manifests/seaweedfs/seaweedfs.yaml new file mode 100644 index 00000000..a2f2d8f9 --- /dev/null +++ b/hack/testing-manifests/seaweedfs/seaweedfs.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: seaweedfs + labels: + name: seaweedfs +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app: seaweedfs + name: seaweedfs-s3 + namespace: seaweedfs +spec: + containers: + - name: seaweedfs + image: chrislusf/seaweedfs:4.18 + args: + - server + - -s3 + - -dir=/data + ports: + - containerPort: 9000 + name: s3 + - containerPort: 9333 + name: master + - containerPort: 8888 + name: filer + volumeMounts: + - mountPath: /data + name: localvolume + volumes: + - name: localvolume + hostPath: + path: /mnt/seaweedfs/data + type: DirectoryOrCreate +--- +apiVersion: v1 +kind: Service +metadata: + name: seaweedfs + namespace: seaweedfs +spec: + selector: + app: seaweedfs + ports: + - port: 8333 + name: s3 diff --git a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml new file mode 100644 index 00000000..0769371c --- /dev/null +++ b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml @@ -0,0 +1,960 @@ +--- +requiredOperatorVersion: ^2.0.0 + +features: + filestreamQueue: false + +bucket: + default: + ingress: + paths: + - /bucket + servicePort: "http-minio" + pathType: Prefix + images: + seaweedfs: + registry: "docker.io" + repository: "chrislusf/seaweedfs" + tag: "4.35" + +clickhouse: + default: + images: + server: + registry: "docker.io" + repository: "altinity/clickhouse-server" + tag: "25.8.16.10002.altinitystable" + +clickhouseKeeper: + default: {} + +generatedSecrets: + - name: session-key + length: 32 + type: password + - name: weave-worker-auth + length: 32 + type: password + useExactName: true + +kafka: + images: + bufstream: + registry: "us-docker.pkg.dev" + repository: "buf-images-1/buf/images/bufstream" + tag: "0.4.15" + etcd: + registry: "quay.io" + repository: "coreos/etcd" + tag: "v3.5.31" + bucketEnsure: + repository: "amazon/aws-cli" + tag: "2.35.10" + topics: + - name: filestream + features: + - filestreamQueue + topic: filestream + partitionCount: 96 + - name: flat-run-fields-updater + topic: flat-run-fields-updater + partitionCount: 96 + - name: weave-worker + topic: weave.call_ended + partitionCount: 16 + - name: weave-evaluate-model-worker + topic: weave.evaluate_model + partitionCount: 16 + +mysql: + default: + images: + mysql: + registry: "ghcr.io" + repository: "cybozu-go/moco/mysql" + tag: "8.4.8" + exporter: + registry: "docker.io" + repository: "prom/mysqld-exporter" + tag: "v0.15.1" + metadata: {} + runs: {} + usage: {} + +redis: + default: + images: + standalone: + registry: "quay.io" + repository: "opstree/redis" + tag: "v7.0.15" + replication: + registry: "quay.io" + repository: "opstree/redis" + tag: "v7.0.15" + sentinel: + registry: "quay.io" + repository: "opstree/redis-sentinel" + tag: "v7.0.12" + exporter: + registry: "quay.io" + repository: "opstree/redis-exporter" + tag: "v1.44.0" + +commonEnvvars: + gorillaMysql: + - name: MYSQL + sources: + - name: default + type: mysql + - name: GORILLA_ANALYTICS_SINK + sources: + - name: default + type: mysql + - name: GORILLA_FILE_STREAM_STORE_ADDRESS + sources: + - name: default + type: mysql + - name: GORILLA_METADATA_STORE + sources: + - name: metadata + type: mysql + - name: GORILLA_RUN_STORE + sources: + - name: runs + type: mysql + - name: GORILLA_USAGE_STORE + sources: + - name: usage + type: mysql + gorillaBucket: + - name: AWS_REGION + sources: + - name: default + type: bucket + field: region + defaultValue: "us-east-1" + - name: BUCKET + sources: + - name: default + type: bucket + - name: GORILLA_FILE_STORE + sources: + - name: default + type: bucket + - name: GORILLA_RUN_UPDATE_SHADOW_QUEUE_OVERFLOW_BUCKET_STORE + sources: + - name: default + type: bucket + - name: GORILLA_STORAGE_BUCKET + sources: + - name: default + type: bucket + gorillaRedis: + - name: REDIS + sources: + - name: default + type: redis + - name: GORILLA_ACTIVITY_STORE_CACHE_ADDRESS + sources: + - name: default + type: redis + - name: GORILLA_AUDITOR_CACHE + sources: + - name: default + type: redis + - name: GORILLA_CACHE + sources: + - name: default + type: redis + - name: GORILLA_FILE_METADATA_SOURCE + sources: + - name: default + type: redis + - name: GORILLA_LOCKER + sources: + - name: default + type: redis + - name: GORILLA_METADATA_CACHE + sources: + - name: default + type: redis + - name: GORILLA_SETTINGS_CACHE + sources: + - name: default + type: redis + - name: GORILLA_USAGE_METRICS_CACHE + sources: + - name: default + type: redis + gorillaService: + - name: GORILLA_SWEEP_PROVIDER + sources: + - name: anaconda2 + type: service + proto: "http" + path: "" + gorillaTaskQueueConsumer: + - name: GORILLA_TASK_QUEUE + sources: + - name: taskQueue + type: redis + params: + concurrency: 10 + - name: GORILLA_TASK_QUEUE_WORKER_ENABLED + value: "true" + gorillaTaskQueueProducer: + - name: GORILLA_TASK_QUEUE + sources: + - name: taskQueue + type: redis + - name: GORILLA_TASK_QUEUE_WORKER_ENABLED + value: "false" + gorillaHistoryStore: + - name: GORILLA_HISTORY_STORE + sources: + - name: parquet + type: service + port: parquet + proto: "http" + path: "/_goRPC_" + - name: default + type: mysql + - name: GORILLA_PARQUET_LIVE_HISTORY_STORE + sources: + - name: default + type: mysql + gorillaOnprem: + - name: GORILLA_LOCAL_SERVICE_BYPASS + value: "true" + - name: GORILLA_DEFAULT_REGION + value: "minio-local" + - name: GORILLA_EMAIL_SINK + sources: + - name: email + type: custom-resource + field: status.emailSink + defaultValue: "https://api.wandb.ai/email/dispatch" + - name: GORILLA_FILE_METADATA_SOURCE_IS_INTERNAL + value: "true" + - name: GORILLA_ONPREM + value: "true" + - name: GORILLA_ONPREM_API_KEY_PREFIX + value: "local" + - name: GORILLA_STATSD_PORT + value: "0" + - name: GORILLA_SESSION_KEY + sources: + - name: session-key + type: generatedSecret + - name: BUCKET_PROXY + sources: + - name: bucket-proxy + type: custom-resource + field: spec.wandb.bucketProxy + - name: GORILLA_GLUE_FILE_STORE_IS_PROXIED + sources: + - name: bucket-proxy + type: custom-resource + field: spec.wandb.bucketProxy + - name: GORILLA_FILE_STORE_IS_PROXIED + sources: + - name: bucket-proxy + type: custom-resource + field: spec.wandb.bucketProxy + - name: GORILLA_FILE_HOST + sources: + - name: hostname + type: custom-resource + field: status.wandb.hostname + - name: GORILLA_FRONTEND_HOST + sources: + - name: hostname + type: custom-resource + field: status.wandb.hostname + - name: LICENSE + sources: + - name: license + type: custom-resource + field: spec.wandb.license + - name: GORILLA_LICENSE + sources: + - name: license + type: custom-resource + field: spec.wandb.license + - name: GORILLA_OIDC_CLIENT_ID + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.clientId + - name: GORILLA_OIDC_SECRET + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.clientSecret + - name: GORILLA_OIDC_ISSUER + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.issuerUrl + - name: GORILLA_AUTH_METHOD + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.authMethod + - name: GORILLA_SESSION_LENGTH + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.sessionLength + defaultValue: "720h" + gorillaCustomerSecrets: + - name: GORILLA_CUSTOMER_SECRET_STORE_SOURCE + value: "k8s-secretmanager://" + - name: GORILLA_CUSTOMER_SECRET_STORE_K8S_CONFIG_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: GORILLA_INTERNAL_JWT_SUBJECTS_TO_ISSUERS + sources: + - name: internal + type: jwt-issuer-map + clickhouse: + - name: WF_CLICKHOUSE_HOST + sources: + - name: default + type: clickhouse + field: host + - name: WF_CLICKHOUSE_PORT + sources: + - name: default + type: clickhouse + field: http-port + - name: WF_CLICKHOUSE_USER + sources: + - name: default + type: clickhouse + field: user + - name: WF_CLICKHOUSE_PASS + sources: + - name: default + type: clickhouse + field: password + - name: WF_CLICKHOUSE_DATABASE + sources: + - name: default + type: clickhouse + field: database + kafka: + - name: KAFKA_BROKER_HOST + sources: + - name: default + type: kafka + field: host + - name: KAFKA_BROKER_PORT + sources: + - name: default + type: kafka + field: port + - name: KAFKA_URL + sources: + - name: default + type: kafka + field: url + weaveTrace: + - name: WANDB_PUBLIC_BASE_URL + sources: + - name: hostname + type: custom-resource + field: status.wandb.hostname + - name: WANDB_BASE_URL + sources: + - name: api + type: service + proto: "http" + path: "/" + - name: WF_TRACE_SERVER_URL + sources: + - name: weave-trace + type: service + proto: "http" + path: "/traces" + - name: WEAVE_TRACE_SERVER_BASE_URL + sources: + - name: weave-trace + type: service + proto: "http" + path: "/traces" + - name: API_PATH_PREFIX + value: "/traces" + - name: WEAVE_ENABLE_ONLINE_EVAL + value: "true" + - name: WEAVE_ENABLE_AGENT_SCORING + value: "false" + - name: WEAVE_ENABLE_EVALUATE_MODEL_WORKER + value: "true" + - name: WANDB_INTERNAL_SERVICE_TOKEN + sources: + - name: weave-worker-auth + type: generatedSecret + - name: WANDB_INTERNAL_SERVICE_TOKEN_SECRET_NAME + value: "weave-worker-auth" + frontend: + - name: REACT_APP_HOST + source: + - name: hostname + type: custom-resource + field: status.wandb.hostname + - name: REACT_APP_ENVIRONMENT_NAME + value: "local" + - name: REACT_APP_ENVIRONMENT_IS_PRIVATE + value: "true" + - name: REACT_APP_ANALYTICS_DISABLED + value: "true" + - name: WEAVE_TRACES_ENABLED + value: "true" + - name: SERVER_FLAG_WEAVE_1_PERCENTAGE + value: "100" + flatRunsV2Producer: + - name: KAFKA_RUNS_V2_TOPIC_NAME + value: "flat-run-fields-updater" + - name: GORILLA_RUN_UPDATE_SHADOW_QUEUE_ADDR + value: "$(KAFKA_URL)/$(KAFKA_RUNS_V2_TOPIC_NAME)" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_CREATE_RUN_TABLES + value: "true" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_CREATE_RUN_STORE + value: "true" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_SHADOW_RUN_UPDATES + value: "true" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_DISABLE_READS + value: "false" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_FLAT_RUNS_MIGRATOR + value: "true" + flatRunsV2Consumer: + - name: KAFKA_RUNS_V2_TOPIC_NAME + value: "flat-run-fields-updater" + telemetryOtel: + - name: OTEL_EXPORTER_OTLP_PROTOCOL + sources: + - type: telemetry + field: protocol + - name: OTEL_TRACES_EXPORTER + sources: + - type: telemetry + field: tracesExporter + - name: OTEL_METRICS_EXPORTER + sources: + - type: telemetry + field: metricsExporter + - name: OTEL_LOGS_EXPORTER + sources: + - type: telemetry + field: logsExporter + - name: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT + sources: + - type: telemetry + field: metricsEndpoint + - name: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT + sources: + - type: telemetry + field: logsEndpoint + - name: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT + sources: + - type: telemetry + field: tracesEndpoint + - name: OTEL_SERVICE_NAME + sources: + - type: telemetry + field: serviceName + - name: OTEL_RESOURCE_ATTRIBUTES + sources: + - type: telemetry + field: resourceAttributes + - name: GORILLA_TRACER + sources: + - type: telemetry + field: gorillaTracer +commonVolumeMounts: + internalSigner: + - mountPath: /vol/env + name: wandb-internal-signer-root + source: + name: wandb-internal-signer + type: secret + +applications: + anaconda2: + name: anaconda2 + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/anaconda2 + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: anaconda2 + ports: + - containerPort: 8080 + name: anaconda2 + protocol: TCP + livenessProbe: + httpGet: + path: /ping + readinessProbe: + httpGet: + path: /ping + service: + ports: + - port: 8080 + protocol: TCP + name: anaconda2 + api: + name: api + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaHistoryStore + - gorillaTaskQueueProducer + - gorillaService + - gorillaOnprem + - gorillaCustomerSecrets + - kafka + - flatRunsV2Producer + commonVolumeMounts: + - internalSigner + env: + - name: GORILLA_LICENSE_CERT_PATH + value: "/jwks.json" + - name: GORILLA_VIEW_SPEC_UPDATER_EXECUTABLE + value: "/view-spec-updater-linux" + - name: INTERNAL_SIGNER_KEY_PATH + value: "/vol/env" + - name: MIGRATE_RUNS_DB + sources: + - name: default + type: mysql + - name: MIGRATE_USAGE_DB + sources: + - name: default + type: mysql + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: api + args: + - gorilla + ports: + - containerPort: 8080 + name: api + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + readinessProbe: + httpGet: + path: /ready + service: + ports: + - port: 8080 + protocol: TCP + name: api + ingress: + paths: + - /api + - /artifacts + - /artifactsV2 + - /files + - /graphql + - /graphql2 + - /oidc + servicePort: "8080" + pathType: Prefix + executor: + name: executor + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaHistoryStore + - gorillaTaskQueueConsumer + - gorillaService + - gorillaOnprem + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: executor + args: + - executor + filemeta: + name: filemeta + args: + - filemeta + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaService + - gorillaOnprem + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + filestream: + name: filestream + features: + - filestreamQueue + args: + - filestream + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaService + - gorillaOnprem + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: filestream + flat-run-fields-updater: + name: flat-run-fields-updater + args: + - flat-run-fields-updater + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaService + - gorillaOnprem + - kafka + - flatRunsV2Consumer + env: + - name: GORILLA_RUN_UPDATE_SHADOW_QUEUE_SUBSCRIPTIONS_FLAT_RUN_FIELDS_UPDATER + value: "$(KAFKA_URL)/$(KAFKA_RUNS_V2_TOPIC_NAME)?consumer_group_id=flat-run-fields-updater" + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: flat-run-fields-updater + frontend: + name: frontend + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/frontend-nginx + tag: 0.83.0-clickhouse-keeper.2 + commonEnvs: + - frontend + env: + - name: FRONTEND_APP_BACKEND + sources: + - name: api + type: service + proto: "" + path: "" + - name: FRONTEND_AUTH_BACKEND + sources: + - name: api + type: service + proto: "" + path: "" + - name: FRONTEND_LOCAL_BACKEND + sources: + - name: api + type: service + proto: "" + path: "" + - name: FRONTEND_WEAVE_BACKEND + sources: + - name: weave + type: service + proto: "" + path: "" + - name: WEAVE_ENABLED + value: "true" + - name: OPERATOR_ENABLED + value: "true" + containers: + - name: frontend + ports: + - containerPort: 8080 + name: frontend + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + readinessProbe: + httpGet: + path: /ready + service: + ports: + - port: 8080 + protocol: TCP + name: frontend + ingress: + paths: + - / + servicePort: "8080" + pathType: Prefix + volumeMounts: + - name: tmp + mountPath: /tmp/ + source: + type: emptyDir + name: tmp + - name: nginx-cache + mountPath: /var/cache/nginx + source: + type: emptyDir + name: nginx-cache + glue: + name: glue + args: + - glue + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaHistoryStore + - gorillaService + - gorillaOnprem + commonVolumeMounts: + - internalSigner + env: + - name: GORILLA_LICENSE_CERT_PATH + value: "/jwks.json" + - name: GORILLA_VIEW_SPEC_UPDATER_EXECUTABLE + value: "/view-spec-updater-linux" + - name: GORILLA_GLUE_TASK_PROVIDER + value: "memory://" + - name: GORILLA_GLUE_TASK_CONFIG_PATH + value: "/gorilla_glue_tasks_local.yaml" + - name: GORILLA_GLUE_TASK_STORE + value: "memory://" + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: glue + metric-observer: + name: metric-observer + args: + - metric-observer + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaService + - gorillaOnprem + - kafka + - flatRunsV2Consumer + env: + - name: GORILLA_RUN_UPDATE_SHADOW_QUEUE_SUBSCRIPTIONS_METRIC_OBSERVER + value: "$(KAFKA_URL)/$(KAFKA_RUNS_V2_TOPIC_NAME)?consumer_group_id=metric-observer" + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: metric-observer + parquet: + name: parquet + args: + - parquet + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaHistoryStore + - gorillaTaskQueueProducer + - gorillaService + - gorillaOnprem + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: parquet + ports: + - containerPort: 8080 + name: parquet + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + readinessProbe: + httpGet: + path: /ready + service: + ports: + - port: 8080 + protocol: TCP + name: parquet + weave: + name: weave + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-python + tag: 0.83.0-clickhouse-keeper.2 + commonEnvs: + env: + - name: DATADOG_TRACE_ENABLED + value: "false" + service: + ports: + - port: 9239 + protocol: TCP + name: weave + ingress: + paths: + - /weave/ + servicePort: "9239" + pathType: Prefix + containers: + - name: weave + ports: + - containerPort: 9239 + name: weave + protocol: TCP + livenessProbe: + httpGet: + path: /__weave/hello + readinessProbe: + httpGet: + path: /__weave/hello + - name: weave-cache-clear + command: + - python + - weave-public/weave_query/scripts/clear_cache.py + resources: + limits: + cpu: 2 + memory: 2Gi + requests: + cpu: 100m + memory: 128Mi + volumeMounts: + - name: temp-dir + mountPath: /tmp/ + source: + type: emptyDir + name: temp-dir + - name: cache + mountPath: /vol/weave/cache + source: + type: emptyDir + name: cache + weave-trace: + name: weave-trace + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-trace + tag: 0.83.0-clickhouse-keeper.2 + commonEnvs: + - clickhouse + - kafka + - weaveTrace + containers: + - name: weave-trace + args: + - "uvicorn" + - "src.trace_server:app" + - "--host" + - "0.0.0.0" + - "--port" + - "8080" + ports: + - containerPort: 8080 + name: weave-trace + protocol: TCP + livenessProbe: + httpGet: + path: /traces/health + readinessProbe: + httpGet: + path: /traces/health + service: + ports: + - port: 8080 + protocol: TCP + name: weave-trace + ingress: + paths: + - /traces + servicePort: "8080" + pathType: Prefix + jwtTokens: + - name: internal-jwt + mountPath: /tmp/weave-trace/internal-jwt + source: + kubernetesServiceAccount: + audience: internal-service + expirationSeconds: 600 + weave-trace-worker: + name: weave-trace-worker + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-trace + tag: 0.83.0-clickhouse-keeper.2 + env: + - name: DD_TRACE_ENABLED + value: "false" + commonEnvs: + - clickhouse + - kafka + - weaveTrace + containers: + - name: weave-trace-worker + args: + - "python" + - "-m" + - "src.workers.scoring_worker" + weave-trace-evaluate-model-worker: + name: weave-trace-evaluate-model-worker + legacyKey: weave-evaluate-model-worker + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-trace + tag: 0.83.0-clickhouse-keeper.2 + env: + - name: DD_TRACE_ENABLED + value: "false" + commonEnvs: + - clickhouse + - kafka + - weaveTrace + containers: + - name: weave-trace-evaluate-model-worker + args: + - "python" + - "-m" + - "src.workers.evaluate_model_worker.evaluate_model_worker" + +migrations: + gorilla: + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + args: + - "migrate" + - "--db=$(GORILLA_METADATA_STORE)" + - "--runs-db=$(GORILLA_RUN_STORE)" + - "--usage-db=$(GORILLA_USAGE_STORE)" + - "--squash" + - "true" + commonEnvs: + - gorillaMysql + - gorillaOnprem + internal-signer: + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + args: + - "secret-generation-job" + env: + - name: INTERNAL_SIGNER_K8S_SECRET_NAME + value: "wandb-internal-signer" + - name: INTERNAL_SIGNER_K8S_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + weave-trace: + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-trace + tag: 0.83.0-clickhouse-keeper.2 + args: + - python + - migrator.py + commonEnvs: + - clickhouse diff --git a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/sizing.yaml b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/sizing.yaml new file mode 100644 index 00000000..29f7223b --- /dev/null +++ b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/sizing.yaml @@ -0,0 +1,1517 @@ +bucket: + default: + sizing: + default: + replicas: 1 + pools: 1 + volumeSize: 10Gi + micro: + replicas: 3 + volumeSize: 50Gi + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + replicas: 3 + volumeSize: 100Gi + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + replicas: 3 + volumeSize: 100Gi + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi + xlarge: + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi + 2xlarge: + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi +clickhouse: + default: + sizing: + default: + shards: 1 + replicas: 1 + volumeSize: 10Gi + micro: + replicas: 2 + volumeSize: 30Gi + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + shards: 1 + replicas: 3 + volumeSize: 50Gi + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + shards: 1 + replicas: 3 + volumeSize: 100Gi + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + shards: 1 + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi + xlarge: + shards: 2 + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi + 2xlarge: + shards: 4 + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi +# Keeper coordinates ReplicatedMergeTree replication; it stores only the Raft +# log + metadata snapshots, so its volume is small and independent of CH data. +# replicas must be odd (Raft quorum): 1 for single-node, 3 for replicated CH. +clickhouseKeeper: + default: + sizing: + default: + replicas: 1 + volumeSize: 10Gi + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 500m + memory: 1Gi + micro: + replicas: 3 + volumeSize: 10Gi + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 500m + memory: 1Gi + small: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 1 + memory: 2Gi + limits: + cpu: 1 + memory: 2Gi + medium: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 1 + memory: 2Gi + limits: + cpu: 1 + memory: 2Gi + large: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 2 + memory: 4Gi + xlarge: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 2 + memory: 4Gi + 2xlarge: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 2 + memory: 4Gi +kafka: + sizing: + # Bufstream brokers are stateless; replicas = broker count (floored at 2), volumeSize = etcd metadata PVC, replicationFactor = topic RF (1 for object-store backed). + default: + replicas: 2 + volumeSize: 10Gi + replicationFactor: 1 + micro: + replicas: 2 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + replicas: 2 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + replicas: 3 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + replicas: 3 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + xlarge: + replicas: 4 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + 2xlarge: + replicas: 6 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi +mysql: + default: + sizing: + default: + replicas: 1 + volumeSize: 10Gi + micro: + replicas: 3 + volumeSize: 50Gi + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + replicas: 3 + volumeSize: 100Gi + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + replicas: 3 + volumeSize: 500Gi + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + xlarge: + replicas: 3 + volumeSize: 1Ti + resources: + requests: + cpu: 15 + memory: 60Gi + limits: + cpu: 15 + memory: 60Gi + 2xlarge: + replicas: 3 + volumeSize: 2Ti + resources: + requests: + cpu: 30 + memory: 120Gi + limits: + cpu: 30 + memory: 120Gi +redis: + default: + sizing: + default: + shards: 1 + replicas: 1 + volumeSize: 5Gi + micro: + replicas: 2 + volumeSize: 8Gi + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + shards: 1 + replicas: 3 + volumeSize: 16Gi + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + shards: 1 + replicas: 3 + volumeSize: 32Gi + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + shards: 1 + replicas: 3 + volumeSize: 64Gi + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + xlarge: + shards: 2 + replicas: 3 + volumeSize: 128Gi + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + 2xlarge: + shards: 4 + replicas: 3 + volumeSize: 256Gi + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi +applications: + anaconda2: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "2" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + medium: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 4 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 4 + api: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + medium: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + large: + resources: + limits: + cpu: "16" + memory: 32Gi + requests: + cpu: "16" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 4 + xlarge: + resources: + limits: + cpu: "16" + memory: 32Gi + requests: + cpu: "16" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 12 + minReplicas: 6 + 2xlarge: + resources: + limits: + cpu: "16" + memory: 32Gi + requests: + cpu: "16" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 16 + minReplicas: 8 + executor: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 4Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "4" + memory: 16Gi + requests: + cpu: "4" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + large: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 16 + minReplicas: 8 + 2xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 32 + minReplicas: 16 + filemeta: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 2Gi + requests: + cpu: "2" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "4" + memory: 4Gi + requests: + cpu: "4" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + filestream: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "2" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + flat-run-fields-updater: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "2" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + frontend: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 2Gi + requests: + cpu: "2" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + medium: + resources: + limits: + cpu: "4" + memory: 4Gi + requests: + cpu: "4" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + large: + resources: + limits: + cpu: "8" + memory: 8Gi + requests: + cpu: "8" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "8" + memory: 8Gi + requests: + cpu: "8" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 8Gi + requests: + cpu: "8" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + glue: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 3Gi + requests: + cpu: "1" + memory: 3Gi + small: + resources: + limits: + cpu: "2" + memory: 6Gi + requests: + cpu: "2" + memory: 6Gi + medium: + resources: + limits: + cpu: "4" + memory: 12Gi + requests: + cpu: "4" + memory: 12Gi + large: + resources: + limits: + cpu: "8" + memory: 24Gi + requests: + cpu: "8" + memory: 24Gi + xlarge: + resources: + limits: + cpu: "16" + memory: 48Gi + requests: + cpu: "16" + memory: 48Gi + 2xlarge: + resources: + limits: + cpu: "30" + memory: 64Gi + requests: + cpu: "30" + memory: 64Gi + metric-observer: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "2" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + parquet: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "4" + memory: 16Gi + requests: + cpu: "4" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 2 + medium: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 2 + large: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 4 + 2xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + weave: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 4Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "4" + memory: 16Gi + requests: + cpu: "4" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 2 + medium: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + large: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 5 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 5 + minReplicas: 2 + weave-trace: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + medium: + resources: + limits: + cpu: "2" + memory: 16Gi + requests: + cpu: "2" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + weave-trace-worker: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 1 + medium: + resources: + limits: + cpu: "2" + memory: 16Gi + requests: + cpu: "2" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + weave-trace-evaluate-model-worker: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "2" + memory: 16Gi + requests: + cpu: "2" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 diff --git a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml new file mode 100644 index 00000000..0769371c --- /dev/null +++ b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml @@ -0,0 +1,960 @@ +--- +requiredOperatorVersion: ^2.0.0 + +features: + filestreamQueue: false + +bucket: + default: + ingress: + paths: + - /bucket + servicePort: "http-minio" + pathType: Prefix + images: + seaweedfs: + registry: "docker.io" + repository: "chrislusf/seaweedfs" + tag: "4.35" + +clickhouse: + default: + images: + server: + registry: "docker.io" + repository: "altinity/clickhouse-server" + tag: "25.8.16.10002.altinitystable" + +clickhouseKeeper: + default: {} + +generatedSecrets: + - name: session-key + length: 32 + type: password + - name: weave-worker-auth + length: 32 + type: password + useExactName: true + +kafka: + images: + bufstream: + registry: "us-docker.pkg.dev" + repository: "buf-images-1/buf/images/bufstream" + tag: "0.4.15" + etcd: + registry: "quay.io" + repository: "coreos/etcd" + tag: "v3.5.31" + bucketEnsure: + repository: "amazon/aws-cli" + tag: "2.35.10" + topics: + - name: filestream + features: + - filestreamQueue + topic: filestream + partitionCount: 96 + - name: flat-run-fields-updater + topic: flat-run-fields-updater + partitionCount: 96 + - name: weave-worker + topic: weave.call_ended + partitionCount: 16 + - name: weave-evaluate-model-worker + topic: weave.evaluate_model + partitionCount: 16 + +mysql: + default: + images: + mysql: + registry: "ghcr.io" + repository: "cybozu-go/moco/mysql" + tag: "8.4.8" + exporter: + registry: "docker.io" + repository: "prom/mysqld-exporter" + tag: "v0.15.1" + metadata: {} + runs: {} + usage: {} + +redis: + default: + images: + standalone: + registry: "quay.io" + repository: "opstree/redis" + tag: "v7.0.15" + replication: + registry: "quay.io" + repository: "opstree/redis" + tag: "v7.0.15" + sentinel: + registry: "quay.io" + repository: "opstree/redis-sentinel" + tag: "v7.0.12" + exporter: + registry: "quay.io" + repository: "opstree/redis-exporter" + tag: "v1.44.0" + +commonEnvvars: + gorillaMysql: + - name: MYSQL + sources: + - name: default + type: mysql + - name: GORILLA_ANALYTICS_SINK + sources: + - name: default + type: mysql + - name: GORILLA_FILE_STREAM_STORE_ADDRESS + sources: + - name: default + type: mysql + - name: GORILLA_METADATA_STORE + sources: + - name: metadata + type: mysql + - name: GORILLA_RUN_STORE + sources: + - name: runs + type: mysql + - name: GORILLA_USAGE_STORE + sources: + - name: usage + type: mysql + gorillaBucket: + - name: AWS_REGION + sources: + - name: default + type: bucket + field: region + defaultValue: "us-east-1" + - name: BUCKET + sources: + - name: default + type: bucket + - name: GORILLA_FILE_STORE + sources: + - name: default + type: bucket + - name: GORILLA_RUN_UPDATE_SHADOW_QUEUE_OVERFLOW_BUCKET_STORE + sources: + - name: default + type: bucket + - name: GORILLA_STORAGE_BUCKET + sources: + - name: default + type: bucket + gorillaRedis: + - name: REDIS + sources: + - name: default + type: redis + - name: GORILLA_ACTIVITY_STORE_CACHE_ADDRESS + sources: + - name: default + type: redis + - name: GORILLA_AUDITOR_CACHE + sources: + - name: default + type: redis + - name: GORILLA_CACHE + sources: + - name: default + type: redis + - name: GORILLA_FILE_METADATA_SOURCE + sources: + - name: default + type: redis + - name: GORILLA_LOCKER + sources: + - name: default + type: redis + - name: GORILLA_METADATA_CACHE + sources: + - name: default + type: redis + - name: GORILLA_SETTINGS_CACHE + sources: + - name: default + type: redis + - name: GORILLA_USAGE_METRICS_CACHE + sources: + - name: default + type: redis + gorillaService: + - name: GORILLA_SWEEP_PROVIDER + sources: + - name: anaconda2 + type: service + proto: "http" + path: "" + gorillaTaskQueueConsumer: + - name: GORILLA_TASK_QUEUE + sources: + - name: taskQueue + type: redis + params: + concurrency: 10 + - name: GORILLA_TASK_QUEUE_WORKER_ENABLED + value: "true" + gorillaTaskQueueProducer: + - name: GORILLA_TASK_QUEUE + sources: + - name: taskQueue + type: redis + - name: GORILLA_TASK_QUEUE_WORKER_ENABLED + value: "false" + gorillaHistoryStore: + - name: GORILLA_HISTORY_STORE + sources: + - name: parquet + type: service + port: parquet + proto: "http" + path: "/_goRPC_" + - name: default + type: mysql + - name: GORILLA_PARQUET_LIVE_HISTORY_STORE + sources: + - name: default + type: mysql + gorillaOnprem: + - name: GORILLA_LOCAL_SERVICE_BYPASS + value: "true" + - name: GORILLA_DEFAULT_REGION + value: "minio-local" + - name: GORILLA_EMAIL_SINK + sources: + - name: email + type: custom-resource + field: status.emailSink + defaultValue: "https://api.wandb.ai/email/dispatch" + - name: GORILLA_FILE_METADATA_SOURCE_IS_INTERNAL + value: "true" + - name: GORILLA_ONPREM + value: "true" + - name: GORILLA_ONPREM_API_KEY_PREFIX + value: "local" + - name: GORILLA_STATSD_PORT + value: "0" + - name: GORILLA_SESSION_KEY + sources: + - name: session-key + type: generatedSecret + - name: BUCKET_PROXY + sources: + - name: bucket-proxy + type: custom-resource + field: spec.wandb.bucketProxy + - name: GORILLA_GLUE_FILE_STORE_IS_PROXIED + sources: + - name: bucket-proxy + type: custom-resource + field: spec.wandb.bucketProxy + - name: GORILLA_FILE_STORE_IS_PROXIED + sources: + - name: bucket-proxy + type: custom-resource + field: spec.wandb.bucketProxy + - name: GORILLA_FILE_HOST + sources: + - name: hostname + type: custom-resource + field: status.wandb.hostname + - name: GORILLA_FRONTEND_HOST + sources: + - name: hostname + type: custom-resource + field: status.wandb.hostname + - name: LICENSE + sources: + - name: license + type: custom-resource + field: spec.wandb.license + - name: GORILLA_LICENSE + sources: + - name: license + type: custom-resource + field: spec.wandb.license + - name: GORILLA_OIDC_CLIENT_ID + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.clientId + - name: GORILLA_OIDC_SECRET + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.clientSecret + - name: GORILLA_OIDC_ISSUER + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.issuerUrl + - name: GORILLA_AUTH_METHOD + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.authMethod + - name: GORILLA_SESSION_LENGTH + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.sessionLength + defaultValue: "720h" + gorillaCustomerSecrets: + - name: GORILLA_CUSTOMER_SECRET_STORE_SOURCE + value: "k8s-secretmanager://" + - name: GORILLA_CUSTOMER_SECRET_STORE_K8S_CONFIG_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: GORILLA_INTERNAL_JWT_SUBJECTS_TO_ISSUERS + sources: + - name: internal + type: jwt-issuer-map + clickhouse: + - name: WF_CLICKHOUSE_HOST + sources: + - name: default + type: clickhouse + field: host + - name: WF_CLICKHOUSE_PORT + sources: + - name: default + type: clickhouse + field: http-port + - name: WF_CLICKHOUSE_USER + sources: + - name: default + type: clickhouse + field: user + - name: WF_CLICKHOUSE_PASS + sources: + - name: default + type: clickhouse + field: password + - name: WF_CLICKHOUSE_DATABASE + sources: + - name: default + type: clickhouse + field: database + kafka: + - name: KAFKA_BROKER_HOST + sources: + - name: default + type: kafka + field: host + - name: KAFKA_BROKER_PORT + sources: + - name: default + type: kafka + field: port + - name: KAFKA_URL + sources: + - name: default + type: kafka + field: url + weaveTrace: + - name: WANDB_PUBLIC_BASE_URL + sources: + - name: hostname + type: custom-resource + field: status.wandb.hostname + - name: WANDB_BASE_URL + sources: + - name: api + type: service + proto: "http" + path: "/" + - name: WF_TRACE_SERVER_URL + sources: + - name: weave-trace + type: service + proto: "http" + path: "/traces" + - name: WEAVE_TRACE_SERVER_BASE_URL + sources: + - name: weave-trace + type: service + proto: "http" + path: "/traces" + - name: API_PATH_PREFIX + value: "/traces" + - name: WEAVE_ENABLE_ONLINE_EVAL + value: "true" + - name: WEAVE_ENABLE_AGENT_SCORING + value: "false" + - name: WEAVE_ENABLE_EVALUATE_MODEL_WORKER + value: "true" + - name: WANDB_INTERNAL_SERVICE_TOKEN + sources: + - name: weave-worker-auth + type: generatedSecret + - name: WANDB_INTERNAL_SERVICE_TOKEN_SECRET_NAME + value: "weave-worker-auth" + frontend: + - name: REACT_APP_HOST + source: + - name: hostname + type: custom-resource + field: status.wandb.hostname + - name: REACT_APP_ENVIRONMENT_NAME + value: "local" + - name: REACT_APP_ENVIRONMENT_IS_PRIVATE + value: "true" + - name: REACT_APP_ANALYTICS_DISABLED + value: "true" + - name: WEAVE_TRACES_ENABLED + value: "true" + - name: SERVER_FLAG_WEAVE_1_PERCENTAGE + value: "100" + flatRunsV2Producer: + - name: KAFKA_RUNS_V2_TOPIC_NAME + value: "flat-run-fields-updater" + - name: GORILLA_RUN_UPDATE_SHADOW_QUEUE_ADDR + value: "$(KAFKA_URL)/$(KAFKA_RUNS_V2_TOPIC_NAME)" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_CREATE_RUN_TABLES + value: "true" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_CREATE_RUN_STORE + value: "true" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_SHADOW_RUN_UPDATES + value: "true" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_DISABLE_READS + value: "false" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_FLAT_RUNS_MIGRATOR + value: "true" + flatRunsV2Consumer: + - name: KAFKA_RUNS_V2_TOPIC_NAME + value: "flat-run-fields-updater" + telemetryOtel: + - name: OTEL_EXPORTER_OTLP_PROTOCOL + sources: + - type: telemetry + field: protocol + - name: OTEL_TRACES_EXPORTER + sources: + - type: telemetry + field: tracesExporter + - name: OTEL_METRICS_EXPORTER + sources: + - type: telemetry + field: metricsExporter + - name: OTEL_LOGS_EXPORTER + sources: + - type: telemetry + field: logsExporter + - name: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT + sources: + - type: telemetry + field: metricsEndpoint + - name: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT + sources: + - type: telemetry + field: logsEndpoint + - name: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT + sources: + - type: telemetry + field: tracesEndpoint + - name: OTEL_SERVICE_NAME + sources: + - type: telemetry + field: serviceName + - name: OTEL_RESOURCE_ATTRIBUTES + sources: + - type: telemetry + field: resourceAttributes + - name: GORILLA_TRACER + sources: + - type: telemetry + field: gorillaTracer +commonVolumeMounts: + internalSigner: + - mountPath: /vol/env + name: wandb-internal-signer-root + source: + name: wandb-internal-signer + type: secret + +applications: + anaconda2: + name: anaconda2 + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/anaconda2 + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: anaconda2 + ports: + - containerPort: 8080 + name: anaconda2 + protocol: TCP + livenessProbe: + httpGet: + path: /ping + readinessProbe: + httpGet: + path: /ping + service: + ports: + - port: 8080 + protocol: TCP + name: anaconda2 + api: + name: api + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaHistoryStore + - gorillaTaskQueueProducer + - gorillaService + - gorillaOnprem + - gorillaCustomerSecrets + - kafka + - flatRunsV2Producer + commonVolumeMounts: + - internalSigner + env: + - name: GORILLA_LICENSE_CERT_PATH + value: "/jwks.json" + - name: GORILLA_VIEW_SPEC_UPDATER_EXECUTABLE + value: "/view-spec-updater-linux" + - name: INTERNAL_SIGNER_KEY_PATH + value: "/vol/env" + - name: MIGRATE_RUNS_DB + sources: + - name: default + type: mysql + - name: MIGRATE_USAGE_DB + sources: + - name: default + type: mysql + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: api + args: + - gorilla + ports: + - containerPort: 8080 + name: api + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + readinessProbe: + httpGet: + path: /ready + service: + ports: + - port: 8080 + protocol: TCP + name: api + ingress: + paths: + - /api + - /artifacts + - /artifactsV2 + - /files + - /graphql + - /graphql2 + - /oidc + servicePort: "8080" + pathType: Prefix + executor: + name: executor + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaHistoryStore + - gorillaTaskQueueConsumer + - gorillaService + - gorillaOnprem + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: executor + args: + - executor + filemeta: + name: filemeta + args: + - filemeta + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaService + - gorillaOnprem + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + filestream: + name: filestream + features: + - filestreamQueue + args: + - filestream + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaService + - gorillaOnprem + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: filestream + flat-run-fields-updater: + name: flat-run-fields-updater + args: + - flat-run-fields-updater + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaService + - gorillaOnprem + - kafka + - flatRunsV2Consumer + env: + - name: GORILLA_RUN_UPDATE_SHADOW_QUEUE_SUBSCRIPTIONS_FLAT_RUN_FIELDS_UPDATER + value: "$(KAFKA_URL)/$(KAFKA_RUNS_V2_TOPIC_NAME)?consumer_group_id=flat-run-fields-updater" + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: flat-run-fields-updater + frontend: + name: frontend + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/frontend-nginx + tag: 0.83.0-clickhouse-keeper.2 + commonEnvs: + - frontend + env: + - name: FRONTEND_APP_BACKEND + sources: + - name: api + type: service + proto: "" + path: "" + - name: FRONTEND_AUTH_BACKEND + sources: + - name: api + type: service + proto: "" + path: "" + - name: FRONTEND_LOCAL_BACKEND + sources: + - name: api + type: service + proto: "" + path: "" + - name: FRONTEND_WEAVE_BACKEND + sources: + - name: weave + type: service + proto: "" + path: "" + - name: WEAVE_ENABLED + value: "true" + - name: OPERATOR_ENABLED + value: "true" + containers: + - name: frontend + ports: + - containerPort: 8080 + name: frontend + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + readinessProbe: + httpGet: + path: /ready + service: + ports: + - port: 8080 + protocol: TCP + name: frontend + ingress: + paths: + - / + servicePort: "8080" + pathType: Prefix + volumeMounts: + - name: tmp + mountPath: /tmp/ + source: + type: emptyDir + name: tmp + - name: nginx-cache + mountPath: /var/cache/nginx + source: + type: emptyDir + name: nginx-cache + glue: + name: glue + args: + - glue + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaHistoryStore + - gorillaService + - gorillaOnprem + commonVolumeMounts: + - internalSigner + env: + - name: GORILLA_LICENSE_CERT_PATH + value: "/jwks.json" + - name: GORILLA_VIEW_SPEC_UPDATER_EXECUTABLE + value: "/view-spec-updater-linux" + - name: GORILLA_GLUE_TASK_PROVIDER + value: "memory://" + - name: GORILLA_GLUE_TASK_CONFIG_PATH + value: "/gorilla_glue_tasks_local.yaml" + - name: GORILLA_GLUE_TASK_STORE + value: "memory://" + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: glue + metric-observer: + name: metric-observer + args: + - metric-observer + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaService + - gorillaOnprem + - kafka + - flatRunsV2Consumer + env: + - name: GORILLA_RUN_UPDATE_SHADOW_QUEUE_SUBSCRIPTIONS_METRIC_OBSERVER + value: "$(KAFKA_URL)/$(KAFKA_RUNS_V2_TOPIC_NAME)?consumer_group_id=metric-observer" + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: metric-observer + parquet: + name: parquet + args: + - parquet + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaHistoryStore + - gorillaTaskQueueProducer + - gorillaService + - gorillaOnprem + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + containers: + - name: parquet + ports: + - containerPort: 8080 + name: parquet + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + readinessProbe: + httpGet: + path: /ready + service: + ports: + - port: 8080 + protocol: TCP + name: parquet + weave: + name: weave + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-python + tag: 0.83.0-clickhouse-keeper.2 + commonEnvs: + env: + - name: DATADOG_TRACE_ENABLED + value: "false" + service: + ports: + - port: 9239 + protocol: TCP + name: weave + ingress: + paths: + - /weave/ + servicePort: "9239" + pathType: Prefix + containers: + - name: weave + ports: + - containerPort: 9239 + name: weave + protocol: TCP + livenessProbe: + httpGet: + path: /__weave/hello + readinessProbe: + httpGet: + path: /__weave/hello + - name: weave-cache-clear + command: + - python + - weave-public/weave_query/scripts/clear_cache.py + resources: + limits: + cpu: 2 + memory: 2Gi + requests: + cpu: 100m + memory: 128Mi + volumeMounts: + - name: temp-dir + mountPath: /tmp/ + source: + type: emptyDir + name: temp-dir + - name: cache + mountPath: /vol/weave/cache + source: + type: emptyDir + name: cache + weave-trace: + name: weave-trace + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-trace + tag: 0.83.0-clickhouse-keeper.2 + commonEnvs: + - clickhouse + - kafka + - weaveTrace + containers: + - name: weave-trace + args: + - "uvicorn" + - "src.trace_server:app" + - "--host" + - "0.0.0.0" + - "--port" + - "8080" + ports: + - containerPort: 8080 + name: weave-trace + protocol: TCP + livenessProbe: + httpGet: + path: /traces/health + readinessProbe: + httpGet: + path: /traces/health + service: + ports: + - port: 8080 + protocol: TCP + name: weave-trace + ingress: + paths: + - /traces + servicePort: "8080" + pathType: Prefix + jwtTokens: + - name: internal-jwt + mountPath: /tmp/weave-trace/internal-jwt + source: + kubernetesServiceAccount: + audience: internal-service + expirationSeconds: 600 + weave-trace-worker: + name: weave-trace-worker + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-trace + tag: 0.83.0-clickhouse-keeper.2 + env: + - name: DD_TRACE_ENABLED + value: "false" + commonEnvs: + - clickhouse + - kafka + - weaveTrace + containers: + - name: weave-trace-worker + args: + - "python" + - "-m" + - "src.workers.scoring_worker" + weave-trace-evaluate-model-worker: + name: weave-trace-evaluate-model-worker + legacyKey: weave-evaluate-model-worker + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-trace + tag: 0.83.0-clickhouse-keeper.2 + env: + - name: DD_TRACE_ENABLED + value: "false" + commonEnvs: + - clickhouse + - kafka + - weaveTrace + containers: + - name: weave-trace-evaluate-model-worker + args: + - "python" + - "-m" + - "src.workers.evaluate_model_worker.evaluate_model_worker" + +migrations: + gorilla: + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + args: + - "migrate" + - "--db=$(GORILLA_METADATA_STORE)" + - "--runs-db=$(GORILLA_RUN_STORE)" + - "--usage-db=$(GORILLA_USAGE_STORE)" + - "--squash" + - "true" + commonEnvs: + - gorillaMysql + - gorillaOnprem + internal-signer: + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.83.0-clickhouse-keeper.2 + args: + - "secret-generation-job" + env: + - name: INTERNAL_SIGNER_K8S_SECRET_NAME + value: "wandb-internal-signer" + - name: INTERNAL_SIGNER_K8S_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + weave-trace: + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-trace + tag: 0.83.0-clickhouse-keeper.2 + args: + - python + - migrator.py + commonEnvs: + - clickhouse diff --git a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/sizing.yaml b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/sizing.yaml new file mode 100644 index 00000000..5a97462b --- /dev/null +++ b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/sizing.yaml @@ -0,0 +1,1518 @@ +bucket: + default: + sizing: + default: + replicas: 1 + pools: 1 + volumeSize: 10Gi + micro: + replicas: 3 + volumeSize: 50Gi + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + replicas: 3 + volumeSize: 100Gi + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + replicas: 3 + volumeSize: 100Gi + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + replicas: 3 + volumeSize: 200Gi + metadataVolumeSize: 40Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi + xlarge: + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi + 2xlarge: + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi +clickhouse: + default: + sizing: + default: + shards: 1 + replicas: 1 + volumeSize: 10Gi + micro: + replicas: 2 + volumeSize: 30Gi + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + shards: 1 + replicas: 3 + volumeSize: 50Gi + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + shards: 1 + replicas: 3 + volumeSize: 100Gi + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + shards: 1 + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi + xlarge: + shards: 2 + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi + 2xlarge: + shards: 4 + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi +# Keeper coordinates ReplicatedMergeTree replication; it stores only the Raft +# log + metadata snapshots, so its volume is small and independent of CH data. +# replicas must be odd (Raft quorum): 1 for single-node, 3 for replicated CH. +clickhouseKeeper: + default: + sizing: + default: + replicas: 1 + volumeSize: 10Gi + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 500m + memory: 1Gi + micro: + replicas: 3 + volumeSize: 10Gi + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 500m + memory: 1Gi + small: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 1 + memory: 2Gi + limits: + cpu: 1 + memory: 2Gi + medium: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 1 + memory: 2Gi + limits: + cpu: 1 + memory: 2Gi + large: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 2 + memory: 4Gi + xlarge: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 2 + memory: 4Gi + 2xlarge: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 2 + memory: 4Gi +kafka: + sizing: + # Bufstream brokers are stateless; replicas = broker count (floored at 2), volumeSize = etcd metadata PVC, replicationFactor = topic RF (1 for object-store backed). + default: + replicas: 2 + volumeSize: 10Gi + replicationFactor: 1 + micro: + replicas: 2 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + replicas: 2 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + replicas: 3 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + replicas: 3 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + xlarge: + replicas: 4 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + 2xlarge: + replicas: 6 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi +mysql: + default: + sizing: + default: + replicas: 1 + volumeSize: 10Gi + micro: + replicas: 3 + volumeSize: 50Gi + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + replicas: 3 + volumeSize: 100Gi + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + replicas: 3 + volumeSize: 500Gi + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + xlarge: + replicas: 3 + volumeSize: 1Ti + resources: + requests: + cpu: 15 + memory: 60Gi + limits: + cpu: 15 + memory: 60Gi + 2xlarge: + replicas: 3 + volumeSize: 2Ti + resources: + requests: + cpu: 30 + memory: 120Gi + limits: + cpu: 30 + memory: 120Gi +redis: + default: + sizing: + default: + shards: 1 + replicas: 1 + volumeSize: 5Gi + micro: + replicas: 2 + volumeSize: 8Gi + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + shards: 1 + replicas: 3 + volumeSize: 16Gi + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + shards: 1 + replicas: 3 + volumeSize: 32Gi + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + shards: 1 + replicas: 3 + volumeSize: 64Gi + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + xlarge: + shards: 2 + replicas: 3 + volumeSize: 128Gi + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + 2xlarge: + shards: 4 + replicas: 3 + volumeSize: 256Gi + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi +applications: + anaconda2: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "2" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + medium: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 4 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 4 + api: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + medium: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + large: + resources: + limits: + cpu: "16" + memory: 32Gi + requests: + cpu: "16" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 4 + xlarge: + resources: + limits: + cpu: "16" + memory: 32Gi + requests: + cpu: "16" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 12 + minReplicas: 6 + 2xlarge: + resources: + limits: + cpu: "16" + memory: 32Gi + requests: + cpu: "16" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 16 + minReplicas: 8 + executor: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 4Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "4" + memory: 16Gi + requests: + cpu: "4" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + large: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 16 + minReplicas: 8 + 2xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 32 + minReplicas: 16 + filemeta: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 2Gi + requests: + cpu: "2" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "4" + memory: 4Gi + requests: + cpu: "4" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + filestream: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "2" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + flat-run-fields-updater: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "2" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + frontend: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 2Gi + requests: + cpu: "2" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + medium: + resources: + limits: + cpu: "4" + memory: 4Gi + requests: + cpu: "4" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + large: + resources: + limits: + cpu: "8" + memory: 8Gi + requests: + cpu: "8" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "8" + memory: 8Gi + requests: + cpu: "8" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 8Gi + requests: + cpu: "8" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + glue: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 3Gi + requests: + cpu: "1" + memory: 3Gi + small: + resources: + limits: + cpu: "2" + memory: 6Gi + requests: + cpu: "2" + memory: 6Gi + medium: + resources: + limits: + cpu: "4" + memory: 12Gi + requests: + cpu: "4" + memory: 12Gi + large: + resources: + limits: + cpu: "8" + memory: 24Gi + requests: + cpu: "8" + memory: 24Gi + xlarge: + resources: + limits: + cpu: "16" + memory: 48Gi + requests: + cpu: "16" + memory: 48Gi + 2xlarge: + resources: + limits: + cpu: "30" + memory: 64Gi + requests: + cpu: "30" + memory: 64Gi + metric-observer: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "2" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + parquet: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "4" + memory: 16Gi + requests: + cpu: "4" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 2 + medium: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 2 + large: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 4 + 2xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + weave: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 4Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "4" + memory: 16Gi + requests: + cpu: "4" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 2 + medium: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + large: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 5 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 5 + minReplicas: 2 + weave-trace: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + medium: + resources: + limits: + cpu: "2" + memory: 16Gi + requests: + cpu: "2" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + weave-trace-worker: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 1 + medium: + resources: + limits: + cpu: "2" + memory: 16Gi + requests: + cpu: "2" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + weave-trace-evaluate-model-worker: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "2" + memory: 16Gi + requests: + cpu: "2" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "4" + memory: 32Gi + requests: + cpu: "4" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 diff --git a/hack/testing-manifests/telemetry/grafana-dev.yaml b/hack/testing-manifests/telemetry/grafana-dev.yaml new file mode 100644 index 00000000..f4575ad7 --- /dev/null +++ b/hack/testing-manifests/telemetry/grafana-dev.yaml @@ -0,0 +1,76 @@ +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: Grafana +metadata: + name: grafana + namespace: default + labels: + dashboards: "grafana" +spec: + config: + log: + mode: "console" + auth: + disable_login_form: "true" + auth.anonymous: + enabled: "true" + org_role: "Admin" + deployment: + spec: + template: + spec: + containers: + - name: grafana + env: + - name: GF_INSTALL_PLUGINS + value: "victoriametrics-metrics-datasource,victoriametrics-logs-datasource" +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDatasource +metadata: + name: victoria-metrics + namespace: default +spec: + instanceSelector: + matchLabels: + dashboards: "grafana" + datasource: + name: VictoriaMetrics + type: victoriametrics-metrics-datasource + access: proxy + url: http://vmsingle-victoria-instance:8428 + isDefault: true + jsonData: + timeInterval: "30s" +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDatasource +metadata: + name: victoria-logs + namespace: default +spec: + instanceSelector: + matchLabels: + dashboards: "grafana" + datasource: + name: VictoriaLogs + type: victoriametrics-logs-datasource + access: proxy + url: http://vlsingle-victoria-logs:9428 + jsonData: + maxLines: 1000 +--- +apiVersion: grafana.integreatly.org/v1beta1 +kind: GrafanaDatasource +metadata: + name: victoria-traces + namespace: default +spec: + instanceSelector: + matchLabels: + dashboards: "grafana" + datasource: + name: VictoriaTraces + type: jaeger + access: proxy + url: http://vtsingle-victoria-traces:10428/select/jaeger diff --git a/hack/testing-manifests/telemetry/infra-metrics-dev.yaml b/hack/testing-manifests/telemetry/infra-metrics-dev.yaml new file mode 100644 index 00000000..24860d6b --- /dev/null +++ b/hack/testing-manifests/telemetry/infra-metrics-dev.yaml @@ -0,0 +1,139 @@ +--- +# MySQL - Percona XtraDB Cluster +# Scrapes mysqld-exporter sidecar from PXC nodes +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: mysql-pxc + namespace: default +spec: + selector: + matchLabels: + app.kubernetes.io/component: pxc + podMetricsEndpoints: + - port: "metrics" + path: /metrics +--- +# MySQL - ProxySQL +# Scrapes ProxySQL built-in Prometheus metrics +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: mysql-proxysql + namespace: default +spec: + selector: + matchLabels: + app.kubernetes.io/component: proxysql + podMetricsEndpoints: + - port: "stats" + path: /metrics +--- +# Kafka - Bufstream +# Scrapes Bufstream broker metrics from the debug/metrics port +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: kafka-brokers + namespace: default +spec: + selector: + matchLabels: + weightsandbiases.apps.wandb.com/component: kafka + podMetricsEndpoints: + - port: "metrics" + path: /metrics +--- +# SeaweedFS Master +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: seaweedfs-master + namespace: default +spec: + selector: + matchLabels: + app.kubernetes.io/managed-by: seaweedfs-operator + app.kubernetes.io/component: master + podMetricsEndpoints: + - port: master-metrics + path: /metrics +--- +# SeaweedFS Volume +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: seaweedfs-volume + namespace: default +spec: + selector: + matchLabels: + app.kubernetes.io/managed-by: seaweedfs-operator + app.kubernetes.io/component: volume + podMetricsEndpoints: + - port: volume-metrics + path: /metrics +--- +# SeaweedFS Filer +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: seaweedfs-filer + namespace: default +spec: + selector: + matchLabels: + app.kubernetes.io/managed-by: seaweedfs-operator + app.kubernetes.io/component: filer + podMetricsEndpoints: + - port: filer-metrics + path: /metrics +--- +# Redis +# Scrapes Redis exporter sidecar metrics +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMPodScrape +metadata: + name: redis + namespace: default +spec: + selector: + matchLabels: + role: replication + podMetricsEndpoints: + - port: "redis-exporter" + path: /metrics +--- +# ClickHouse Metrics Service +# Service to expose ClickHouse Prometheus metrics on port 9363 +apiVersion: v1 +kind: Service +metadata: + name: clickhouse-metrics + namespace: default + labels: + app: clickhouse-metrics +spec: + selector: + clickhouse.altinity.com/chi: wandb-ch-install + ports: + - name: metrics + port: 9363 + targetPort: 9363 + protocol: TCP + clusterIP: None +--- +# ClickHouse +# Scrapes ClickHouse built-in Prometheus endpoint via Service +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMServiceScrape +metadata: + name: clickhouse + namespace: default +spec: + selector: + matchLabels: + app: clickhouse-metrics + endpoints: + - port: metrics + path: /metrics diff --git a/hack/testing-manifests/telemetry/kube-metrics-dev.yaml b/hack/testing-manifests/telemetry/kube-metrics-dev.yaml new file mode 100644 index 00000000..28aa3153 --- /dev/null +++ b/hack/testing-manifests/telemetry/kube-metrics-dev.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMNodeScrape +metadata: + name: kubelet-cadvisor + namespace: default +spec: + scheme: https + tlsConfig: + insecureSkipVerify: true + caFile: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + relabelConfigs: + - action: labelmap + regex: __meta_kubernetes_node_label_(.+) + - targetLabel: __address__ + replacement: kubernetes.default.svc:443 + - sourceLabels: [__meta_kubernetes_node_name] + regex: (.+) + targetLabel: __metrics_path__ + replacement: /api/v1/nodes/$1/proxy/metrics/cadvisor diff --git a/hack/testing-manifests/telemetry/operator-metrics-dev.yaml b/hack/testing-manifests/telemetry/operator-metrics-dev.yaml new file mode 100644 index 00000000..2400b66b --- /dev/null +++ b/hack/testing-manifests/telemetry/operator-metrics-dev.yaml @@ -0,0 +1,61 @@ +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMServiceScrape +metadata: + name: wandb-operator + namespace: default +spec: + selector: + matchLabels: + control-plane: controller-manager + namespaceSelector: + matchNames: + - operator-system + endpoints: + - port: https + scheme: http +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMServiceScrape +metadata: + name: clickhouse-operator + namespace: default +spec: + selector: + matchLabels: + app: clickhouse-operator + namespaceSelector: + matchNames: + - default + endpoints: + - port: metrics +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMServiceScrape +metadata: + name: grafana-operator + namespace: default +spec: + selector: + matchLabels: + app.kubernetes.io/name: grafana-operator + namespaceSelector: + matchNames: + - default + endpoints: + - port: metrics +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMServiceScrape +metadata: + name: victoria-metrics-operator + namespace: default +spec: + selector: + matchLabels: + app.kubernetes.io/name: victoria-metrics-operator + namespaceSelector: + matchNames: + - default + endpoints: + - port: http diff --git a/hack/testing-manifests/telemetry/victoria-dev.yaml b/hack/testing-manifests/telemetry/victoria-dev.yaml new file mode 100644 index 00000000..cf06fca0 --- /dev/null +++ b/hack/testing-manifests/telemetry/victoria-dev.yaml @@ -0,0 +1,36 @@ +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMSingle +metadata: + name: victoria-instance + namespace: default +spec: + retentionPeriod: "1d" + extraArgs: + opentelemetry.usePrometheusNaming: "true" +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMAgent +metadata: + name: victoria-agent + namespace: default +spec: + selectAllByDefault: true + remoteWrite: + - url: "http://vmsingle-victoria-instance:8428/api/v1/write" +--- +apiVersion: operator.victoriametrics.com/v1 +kind: VLSingle +metadata: + name: victoria-logs + namespace: default +spec: + retentionPeriod: "1d" +--- +apiVersion: operator.victoriametrics.com/v1 +kind: VTSingle +metadata: + name: victoria-traces + namespace: default +spec: + retentionPeriod: "1d" diff --git a/hack/testing-manifests/telemetry/wandb-otel-connection-dev.yaml b/hack/testing-manifests/telemetry/wandb-otel-connection-dev.yaml new file mode 100644 index 00000000..fa54d934 --- /dev/null +++ b/hack/testing-manifests/telemetry/wandb-otel-connection-dev.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: wandb-otel-connection + namespace: default +type: Opaque +stringData: + OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf" + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: "http://vmsingle-victoria-instance:8428/opentelemetry/v1/metrics" + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: "http://vlsingle-victoria-logs:9428/insert/opentelemetry/v1/logs" + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "http://vtsingle-victoria-traces:10428/insert/opentelemetry/v1/traces" + OTEL_SERVICE_NAME: "wandb-service" + OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=dev" diff --git a/hack/testing-manifests/test-infra/Chart.yaml b/hack/testing-manifests/test-infra/Chart.yaml new file mode 100644 index 00000000..d2e84eee --- /dev/null +++ b/hack/testing-manifests/test-infra/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: test-infra +description: Standalone single-container MySQL / Redis / SeaweedFS for testing user-provided infrastructure against the wandb operator. Not for production use. +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/hack/testing-manifests/test-infra/examples/wandb-external-v1.yaml b/hack/testing-manifests/test-infra/examples/wandb-external-v1.yaml new file mode 100644 index 00000000..f87d5d60 --- /dev/null +++ b/hack/testing-manifests/test-infra/examples/wandb-external-v1.yaml @@ -0,0 +1,95 @@ +# v1 WeightsAndBiases CR that exercises the conversion webhook by referencing +# the same Secrets produced by the test-infra helm chart, but expressed in v1's +# `spec.values.global.{mysql,redis,bucket}` shape. +# +# After apply, the v1 → v2 conversion (see api/v1/weightsandbiases_conversion_mapping.go) +# is expected to: +# +# - Map every `{valueFrom: {secretKeyRef: {name, key}}}` entry under +# `global.mysql` to spec.mysql.externalMysql.<field> (note: v1 calls the +# username field `user`; it converts to v2 `username`). +# - Map every `{valueFrom: ...}` entry under `global.redis` to +# spec.redis.externalRedis.<field>. +# - Map `global.bucket.secret.{secretName, accessKeyName, secretKeyName}` to +# spec.objectStore.externalObjectStore.{AccessKey, SecretKey}. The literal +# fields (provider, name, region) currently have no typed v2 home and land +# in the `legacy.operator.wandb.com/bucket-pending` annotation for a later +# reconciler pass to materialize. +# +# Verify after apply: +# kubectl -n test-infra get weightsandbiases.v2.apps.wandb.com wandb-external-v1 -o yaml +# and confirm spec.mysql.externalMysql / spec.redis.externalRedis / +# spec.objectStore.externalObjectStore are populated as expected. +--- +apiVersion: apps.wandb.com/v1 +kind: WeightsAndBiases +metadata: + name: wandb-external-v1 + namespace: test-infra +spec: + chart: + url: https://charts.wandb.ai + name: operator-wandb + version: "0.37.1" + values: + global: + size: dev + host: http://wandb.localhost:8080 + + mysql: + host: + valueFrom: + secretKeyRef: + name: external-mysql-connection + key: Host + port: + valueFrom: + secretKeyRef: + name: external-mysql-connection + key: Port + database: + valueFrom: + secretKeyRef: + name: external-mysql-connection + key: Database + user: + valueFrom: + secretKeyRef: + name: external-mysql-connection + key: Username + password: + valueFrom: + secretKeyRef: + name: external-mysql-connection + key: Password + + redis: + host: + valueFrom: + secretKeyRef: + name: external-redis-connection + key: Host + port: + valueFrom: + secretKeyRef: + name: external-redis-connection + key: Port + + bucket: + provider: s3 + name: wandb + region: us-east-1 + secret: + secretName: external-objectstore-connection + accessKeyName: AccessKey + secretKeyName: SecretKey + + app: + image: + tag: "0.81.0-rc.1779120597" + api: + image: + tag: "0.81.0-rc.1779120597" + + ingress: + class: nginx \ No newline at end of file diff --git a/hack/testing-manifests/test-infra/examples/wandb-external-v2.yaml b/hack/testing-manifests/test-infra/examples/wandb-external-v2.yaml new file mode 100644 index 00000000..2ed88709 --- /dev/null +++ b/hack/testing-manifests/test-infra/examples/wandb-external-v2.yaml @@ -0,0 +1,77 @@ +# v2 WeightsAndBiases CR that wires all three external infra components to the +# Secrets produced by the test-infra helm chart. Apply into the same namespace +# as the chart install so the SecretKeySelector references resolve. +# +# The `URL` field on each connection is intentionally omitted — it's marked +# "generated by operator" in api/v2/weightsandbiases_types.go, so we never +# pre-populate it from the chart's secrets (the chart doesn't emit a `url` key). +--- +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb-external-v2 + namespace: test-infra +spec: + wandb: + hostname: http://localhost:8080 + internalServiceAuth: + enabled: true + features: + proxy: true + version: "0.81.0-rc.1779120597" + size: dev + retentionPolicy: + onDelete: purge + mysql: + externalMysql: + host: + name: external-mysql-connection + key: Host + port: + name: external-mysql-connection + key: Port + database: + name: external-mysql-connection + key: Database + username: + name: external-mysql-connection + key: Username + password: + name: external-mysql-connection + key: Password + redis: + externalRedis: + host: + name: external-redis-connection + key: Host + port: + name: external-redis-connection + key: Port + objectStore: + externalObjectStore: + endpoint: + name: external-objectstore-connection + key: Host + port: + name: external-objectstore-connection + key: Port + bucket: + name: external-objectstore-connection + key: Bucket + region: + name: external-objectstore-connection + key: Region + accessKey: + name: external-objectstore-connection + key: AccessKey + secretKey: + name: external-objectstore-connection + key: SecretKey + kafka: + managedKafka: + telemetry: + enabled: true + clickhouse: + managedClickhouse: + telemetry: + enabled: true diff --git a/hack/testing-manifests/test-infra/templates/_helpers.tpl b/hack/testing-manifests/test-infra/templates/_helpers.tpl new file mode 100644 index 00000000..2e800a8e --- /dev/null +++ b/hack/testing-manifests/test-infra/templates/_helpers.tpl @@ -0,0 +1,48 @@ +{{- define "test-infra.labels" -}} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/part-of: test-infra +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }} +{{- end -}} + +{{- define "test-infra.componentLabels" -}} +{{ include "test-infra.labels" .root }} +app.kubernetes.io/name: {{ .component }} +app.kubernetes.io/component: {{ .component }} +{{- end -}} + +{{- define "test-infra.mysqlHost" -}} +{{ .Values.mysql.service.name }}.{{ .Release.Namespace }}.svc.cluster.local +{{- end -}} + +{{- define "test-infra.redisHost" -}} +{{ .Values.redis.service.name }}.{{ .Release.Namespace }}.svc.cluster.local +{{- end -}} + +{{- define "test-infra.seaweedfsHost" -}} +{{ .Values.seaweedfs.service.name }}.{{ .Release.Namespace }}.svc.cluster.local +{{- end -}} + +{{- define "test-infra.mysqlTLS" -}} +{{- if .Values.mysql.tls.enabled -}}true{{- end -}} +{{- end -}} + +{{- define "test-infra.redisTLS" -}} +{{- if .Values.redis.tls.enabled -}}true{{- end -}} +{{- end -}} + +{{- define "test-infra.anyTLS" -}} +{{- if or .Values.tls.enabled .Values.mysql.tls.enabled .Values.redis.tls.enabled -}}true{{- end -}} +{{- end -}} + +{{- define "test-infra.selfSignedIssuerName" -}} +{{ .Values.tls.issuer.selfSignedName }} +{{- end -}} + +{{- define "test-infra.caIssuerName" -}} +{{ .Values.tls.issuer.caName }} +{{- end -}} + +{{- define "test-infra.caSecretName" -}} +{{ .Values.tls.ca.secretName }} +{{- end -}} diff --git a/hack/testing-manifests/test-infra/templates/certificates.yaml b/hack/testing-manifests/test-infra/templates/certificates.yaml new file mode 100644 index 00000000..16b6d312 --- /dev/null +++ b/hack/testing-manifests/test-infra/templates/certificates.yaml @@ -0,0 +1,100 @@ +{{- if include "test-infra.anyTLS" . }} +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ include "test-infra.selfSignedIssuerName" . }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "tls") | nindent 4 }} +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ include "test-infra.caSecretName" . }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "tls") | nindent 4 }} +spec: + secretName: {{ include "test-infra.caSecretName" . }} + isCA: true + commonName: test-infra-ca + duration: {{ .Values.tls.duration }} + usages: + - cert sign + - crl sign + privateKey: + algorithm: RSA + size: 2048 + issuerRef: + name: {{ include "test-infra.selfSignedIssuerName" . }} + kind: Issuer + group: cert-manager.io +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ include "test-infra.caIssuerName" . }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "tls") | nindent 4 }} +spec: + ca: + secretName: {{ include "test-infra.caSecretName" . }} +{{- if include "test-infra.mysqlTLS" . }} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ .Values.mysql.tls.secretName }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "mysql") | nindent 4 }} +spec: + secretName: {{ .Values.mysql.tls.secretName }} + commonName: {{ include "test-infra.mysqlHost" . }} + duration: {{ .Values.tls.duration }} + usages: + - digital signature + - key encipherment + - server auth + dnsNames: + - {{ .Values.mysql.service.name | quote }} + - {{ printf "%s.%s" .Values.mysql.service.name .Release.Namespace | quote }} + - {{ printf "%s.%s.svc" .Values.mysql.service.name .Release.Namespace | quote }} + - {{ include "test-infra.mysqlHost" . | quote }} + privateKey: + algorithm: RSA + size: 2048 + issuerRef: + name: {{ include "test-infra.caIssuerName" . }} + kind: Issuer + group: cert-manager.io +{{- end }} +{{- if include "test-infra.redisTLS" . }} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ .Values.redis.tls.secretName }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "redis") | nindent 4 }} +spec: + secretName: {{ .Values.redis.tls.secretName }} + commonName: {{ include "test-infra.redisHost" . }} + duration: {{ .Values.tls.duration }} + usages: + - digital signature + - key encipherment + - server auth + dnsNames: + - {{ .Values.redis.service.name | quote }} + - {{ printf "%s.%s" .Values.redis.service.name .Release.Namespace | quote }} + - {{ printf "%s.%s.svc" .Values.redis.service.name .Release.Namespace | quote }} + - {{ include "test-infra.redisHost" . | quote }} + privateKey: + algorithm: RSA + size: 2048 + issuerRef: + name: {{ include "test-infra.caIssuerName" . }} + kind: Issuer + group: cert-manager.io +{{- end }} +{{- end }} diff --git a/hack/testing-manifests/test-infra/templates/mysql-deployment.yaml b/hack/testing-manifests/test-infra/templates/mysql-deployment.yaml new file mode 100644 index 00000000..0eb425aa --- /dev/null +++ b/hack/testing-manifests/test-infra/templates/mysql-deployment.yaml @@ -0,0 +1,81 @@ +{{- if .Values.mysql.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.mysql.service.name }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "mysql") | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/name: mysql + template: + metadata: + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "mysql") | nindent 8 }} + spec: + containers: + - name: mysql + image: {{ .Values.mysql.image }} + imagePullPolicy: IfNotPresent + {{- if include "test-infra.mysqlTLS" . }} + args: + - --require-secure-transport=ON + - --ssl-ca=/etc/mysql/tls/ca.crt + - --ssl-cert=/etc/mysql/tls/tls.crt + - --ssl-key=/etc/mysql/tls/tls.key + {{- end }} + ports: + - name: mysql + containerPort: 3306 + env: + - name: MYSQL_ROOT_PASSWORD + value: {{ .Values.mysql.rootPassword | quote }} + - name: MYSQL_DATABASE + valueFrom: + secretKeyRef: + name: {{ .Values.mysql.secret.name }} + key: Database + - name: MYSQL_USER + valueFrom: + secretKeyRef: + name: {{ .Values.mysql.secret.name }} + key: Username + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.mysql.secret.name }} + key: Password + readinessProbe: + tcpSocket: + port: 3306 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + tcpSocket: + port: 3306 + initialDelaySeconds: 30 + periodSeconds: 10 + resources: + {{- toYaml .Values.mysql.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /var/lib/mysql + {{- if include "test-infra.mysqlTLS" . }} + - name: tls + mountPath: /etc/mysql/tls + readOnly: true + {{- end }} + volumes: + - name: data + emptyDir: {} + {{- if include "test-infra.mysqlTLS" . }} + - name: tls + secret: + secretName: {{ .Values.mysql.tls.secretName }} + {{- end }} +{{- end }} diff --git a/hack/testing-manifests/test-infra/templates/mysql-secret.yaml b/hack/testing-manifests/test-infra/templates/mysql-secret.yaml new file mode 100644 index 00000000..c6c992ab --- /dev/null +++ b/hack/testing-manifests/test-infra/templates/mysql-secret.yaml @@ -0,0 +1,15 @@ +{{- if .Values.mysql.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.mysql.secret.name }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "mysql") | nindent 4 }} +type: Opaque +stringData: + Host: {{ include "test-infra.mysqlHost" . | quote }} + Port: {{ .Values.mysql.service.port | quote }} + Database: {{ .Values.mysql.database | quote }} + Username: {{ .Values.mysql.username | quote }} + Password: {{ .Values.mysql.password | quote }} +{{- end }} diff --git a/hack/testing-manifests/test-infra/templates/mysql-service.yaml b/hack/testing-manifests/test-infra/templates/mysql-service.yaml new file mode 100644 index 00000000..1ee3350e --- /dev/null +++ b/hack/testing-manifests/test-infra/templates/mysql-service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.mysql.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.mysql.service.name }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "mysql") | nindent 4 }} +spec: + type: ClusterIP + selector: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/name: mysql + ports: + - name: mysql + port: {{ .Values.mysql.service.port }} + targetPort: 3306 +{{- end }} diff --git a/hack/testing-manifests/test-infra/templates/redis-deployment.yaml b/hack/testing-manifests/test-infra/templates/redis-deployment.yaml new file mode 100644 index 00000000..d27ef5fd --- /dev/null +++ b/hack/testing-manifests/test-infra/templates/redis-deployment.yaml @@ -0,0 +1,85 @@ +{{- if .Values.redis.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.redis.service.name }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "redis") | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/name: redis + template: + metadata: + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "redis") | nindent 8 }} + spec: + containers: + - name: redis + image: {{ .Values.redis.image }} + imagePullPolicy: IfNotPresent + {{- if include "test-infra.redisTLS" . }} + command: + - /bin/sh + - -c + args: + - > + exec redis-server + --port 0 + --tls-port {{ .Values.redis.service.port }} + --tls-cert-file /etc/redis/tls/tls.crt + --tls-key-file /etc/redis/tls/tls.key + --tls-ca-cert-file /etc/redis/tls/ca.crt + --tls-auth-clients no{{- if .Values.redis.password }} --requirepass "$REDIS_PASSWORD"{{- end }} + {{- else if .Values.redis.password }} + command: + - /bin/sh + - -c + - exec redis-server --requirepass "$REDIS_PASSWORD" + {{- else }} + args: ["redis-server"] + {{- end }} + {{- if .Values.redis.password }} + env: + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.redis.secret.name }} + key: Password + {{- end }} + ports: + - name: redis + containerPort: 6379 + readinessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 15 + periodSeconds: 10 + resources: + {{- toYaml .Values.redis.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /data + {{- if include "test-infra.redisTLS" . }} + - name: tls + mountPath: /etc/redis/tls + readOnly: true + {{- end }} + volumes: + - name: data + emptyDir: {} + {{- if include "test-infra.redisTLS" . }} + - name: tls + secret: + secretName: {{ .Values.redis.tls.secretName }} + {{- end }} +{{- end }} diff --git a/hack/testing-manifests/test-infra/templates/redis-secret.yaml b/hack/testing-manifests/test-infra/templates/redis-secret.yaml new file mode 100644 index 00000000..976bef8c --- /dev/null +++ b/hack/testing-manifests/test-infra/templates/redis-secret.yaml @@ -0,0 +1,13 @@ +{{- if .Values.redis.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.redis.secret.name }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "redis") | nindent 4 }} +type: Opaque +stringData: + Host: {{ include "test-infra.redisHost" . | quote }} + Port: {{ .Values.redis.service.port | quote }} + Password: {{ .Values.redis.password | quote }} +{{- end }} diff --git a/hack/testing-manifests/test-infra/templates/redis-service.yaml b/hack/testing-manifests/test-infra/templates/redis-service.yaml new file mode 100644 index 00000000..7d3c8814 --- /dev/null +++ b/hack/testing-manifests/test-infra/templates/redis-service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.redis.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.redis.service.name }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "redis") | nindent 4 }} +spec: + type: ClusterIP + selector: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/name: redis + ports: + - name: redis + port: {{ .Values.redis.service.port }} + targetPort: 6379 +{{- end }} diff --git a/hack/testing-manifests/test-infra/templates/seaweedfs-create-bucket-job.yaml b/hack/testing-manifests/test-infra/templates/seaweedfs-create-bucket-job.yaml new file mode 100644 index 00000000..f2c920ed --- /dev/null +++ b/hack/testing-manifests/test-infra/templates/seaweedfs-create-bucket-job.yaml @@ -0,0 +1,58 @@ +{{- if .Values.seaweedfs.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Values.seaweedfs.service.name }}-create-bucket + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "seaweedfs") | nindent 4 }} + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + backoffLimit: 6 + template: + metadata: + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "seaweedfs") | nindent 8 }} + spec: + restartPolicy: OnFailure + containers: + - name: aws + image: {{ .Values.seaweedfs.awsCliImage }} + imagePullPolicy: IfNotPresent + env: + - name: S3_ENDPOINT + value: "http://{{ include "test-infra.seaweedfsHost" . }}:{{ .Values.seaweedfs.service.apiPort }}" + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ .Values.seaweedfs.secret.name }} + key: AccessKey + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.seaweedfs.secret.name }} + key: SecretKey + - name: AWS_DEFAULT_REGION + valueFrom: + secretKeyRef: + name: {{ .Values.seaweedfs.secret.name }} + key: Region + - name: BUCKET + valueFrom: + secretKeyRef: + name: {{ .Values.seaweedfs.secret.name }} + key: Bucket + command: + - /bin/sh + - -c + - | + set -e + until aws --endpoint-url "$S3_ENDPOINT" s3 ls >/dev/null 2>&1; do + echo "waiting for seaweedfs at $S3_ENDPOINT..." + sleep 2 + done + aws --endpoint-url "$S3_ENDPOINT" s3 mb "s3://$BUCKET" 2>/dev/null || true + aws --endpoint-url "$S3_ENDPOINT" s3 ls "s3://$BUCKET" +{{- end }} diff --git a/hack/testing-manifests/test-infra/templates/seaweedfs-deployment.yaml b/hack/testing-manifests/test-infra/templates/seaweedfs-deployment.yaml new file mode 100644 index 00000000..d84c47d6 --- /dev/null +++ b/hack/testing-manifests/test-infra/templates/seaweedfs-deployment.yaml @@ -0,0 +1,81 @@ +{{- if .Values.seaweedfs.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.seaweedfs.service.name }}-s3-config + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "seaweedfs") | nindent 4 }} +data: + s3.json: | + { + "identities": [ + { + "name": "admin", + "credentials": [ + { + "accessKey": {{ .Values.seaweedfs.accessKey | quote }}, + "secretKey": {{ .Values.seaweedfs.secretKey | quote }} + } + ], + "actions": ["Admin"] + } + ] + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.seaweedfs.service.name }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "seaweedfs") | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/name: seaweedfs + template: + metadata: + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "seaweedfs") | nindent 8 }} + spec: + containers: + - name: seaweedfs + image: {{ .Values.seaweedfs.image }} + imagePullPolicy: IfNotPresent + args: + - server + - -dir=/data + - -s3 + - -s3.config=/etc/seaweedfs/s3.json + ports: + - name: s3 + containerPort: 8333 + readinessProbe: + httpGet: + path: /status + port: 8333 + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + tcpSocket: + port: 8333 + initialDelaySeconds: 30 + periodSeconds: 10 + resources: + {{- toYaml .Values.seaweedfs.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /data + - name: s3-config + mountPath: /etc/seaweedfs + readOnly: true + volumes: + - name: data + emptyDir: {} + - name: s3-config + configMap: + name: {{ .Values.seaweedfs.service.name }}-s3-config +{{- end }} diff --git a/hack/testing-manifests/test-infra/templates/seaweedfs-secret.yaml b/hack/testing-manifests/test-infra/templates/seaweedfs-secret.yaml new file mode 100644 index 00000000..05f0b0eb --- /dev/null +++ b/hack/testing-manifests/test-infra/templates/seaweedfs-secret.yaml @@ -0,0 +1,17 @@ +{{- if .Values.seaweedfs.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.seaweedfs.secret.name }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "seaweedfs") | nindent 4 }} +type: Opaque +stringData: + Provider: {{ .Values.seaweedfs.provider | quote }} + Host: {{ include "test-infra.seaweedfsHost" . | quote }} + Port: {{ .Values.seaweedfs.service.apiPort | quote }} + Bucket: {{ .Values.seaweedfs.bucket | quote }} + Region: {{ .Values.seaweedfs.region | quote }} + AccessKey: {{ .Values.seaweedfs.accessKey | quote }} + SecretKey: {{ .Values.seaweedfs.secretKey | quote }} +{{- end }} diff --git a/hack/testing-manifests/test-infra/templates/seaweedfs-service.yaml b/hack/testing-manifests/test-infra/templates/seaweedfs-service.yaml new file mode 100644 index 00000000..d95d599a --- /dev/null +++ b/hack/testing-manifests/test-infra/templates/seaweedfs-service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.seaweedfs.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.seaweedfs.service.name }} + labels: + {{- include "test-infra.componentLabels" (dict "root" . "component" "seaweedfs") | nindent 4 }} +spec: + type: ClusterIP + selector: + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/name: seaweedfs + ports: + - name: s3 + port: {{ .Values.seaweedfs.service.apiPort }} + targetPort: 8333 +{{- end }} diff --git a/hack/testing-manifests/test-infra/values.yaml b/hack/testing-manifests/test-infra/values.yaml new file mode 100644 index 00000000..9b59f9f8 --- /dev/null +++ b/hack/testing-manifests/test-infra/values.yaml @@ -0,0 +1,64 @@ +tls: + enabled: false + ca: + secretName: test-infra-ca + issuer: + selfSignedName: test-infra-selfsigned + caName: test-infra-ca + duration: 2160h + +mysql: + enabled: true + image: mysql:8.0 + rootPassword: wandb + database: wandb_local + username: wandb + password: wandb + service: + name: mysql + port: 3306 + secret: + name: external-mysql-connection + tls: + enabled: false + secretName: external-mysql-tls + resources: + requests: + cpu: 100m + memory: 256Mi + +redis: + enabled: true + image: redis:7 + password: "" + service: + name: redis + port: 6379 + secret: + name: external-redis-connection + tls: + enabled: false + secretName: external-redis-tls + resources: + requests: + cpu: 50m + memory: 128Mi + +seaweedfs: + enabled: true + image: chrislusf/seaweedfs:latest + awsCliImage: amazon/aws-cli:latest + provider: s3 + accessKey: seaweedfs + secretKey: seaweedfs123 + bucket: wandb + region: us-east-1 + service: + name: seaweedfs + apiPort: 8333 + secret: + name: external-objectstore-connection + resources: + requests: + cpu: 100m + memory: 256Mi diff --git a/hack/testing-manifests/wandb/.generated/.gitkeep b/hack/testing-manifests/wandb/.generated/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/hack/testing-manifests/wandb/custom-ca-e2e/README.md b/hack/testing-manifests/wandb/custom-ca-e2e/README.md new file mode 100644 index 00000000..7e75740e --- /dev/null +++ b/hack/testing-manifests/wandb/custom-ca-e2e/README.md @@ -0,0 +1,52 @@ +# Custom CA Tilt E2E + +This runbook exercises operator v2 custom CA parity with composable Tilt +settings. Tilt generates test CA material at render time, writes it through the +normal W&B CR and user ConfigMap inputs, and installs TLS-enabled external +MySQL/Redis only when those external services are selected. + +## Full Parity Run + +Use all external infra plus custom CA: + +```python +SETTINGS = { + "includeCR": True, + "wandbNamespace": "wandb-ca-e2e", + "useExternalMysql": True, + "useExternalRedis": True, + "useExternalObjectStore": True, + "useCustomCA": True, +} +``` + +Then run: + +```bash +tilt up +``` + +Wait for `Test-Infra`, `wandb-operator`, `Wandb`, and `Wandb-Endpoint`, then +run the verifier from a terminal: + +```bash +./hack/scripts/verify-custom-ca-e2e.sh --namespace wandb-ca-e2e --name wandb +``` + +## Composable Variants + +The settings are independent: + +- `useExternalMysql=True` installs local MySQL from `test-infra` and points the + W&B CR at the generated connection Secret. +- `useExternalRedis=True` does the same for Redis. +- `useExternalObjectStore=True` does the same for SeaweedFS/S3. +- `useCustomCA=True` generates global custom CA material. If external MySQL or + Redis are also enabled, their test-infra services use TLS and the CR includes + `sslCa` selectors for the generated CA Secrets. + +## Clean Up + +```bash +./hack/scripts/tilt-down-dev-clean.sh --namespace wandb-ca-e2e --name wandb +``` diff --git a/hack/testing-manifests/wandb/kustomize/base/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/base/kustomization.yaml new file mode 100644 index 00000000..09ce2acd --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/base/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - wandb.yaml diff --git a/hack/testing-manifests/wandb/kustomize/base/wandb.yaml b/hack/testing-manifests/wandb/kustomize/base/wandb.yaml new file mode 100644 index 00000000..cbaadd19 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/base/wandb.yaml @@ -0,0 +1,36 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + wandb: + hostname: http://localhost + internalServiceAuth: + enabled: true + features: + proxy: true + manifestRepository: "file:///server-manifest" + version: "0.79.0" + size: dev + retentionPolicy: + onDelete: detach + mysql: + managedMysql: + telemetry: + enabled: true + redis: + managedRedis: + telemetry: + enabled: true + kafka: + managedKafka: + telemetry: + enabled: true + objectStore: + managedObjectStore: + telemetry: + enabled: true + clickhouse: + managedClickhouse: + telemetry: + enabled: true diff --git a/hack/testing-manifests/wandb/kustomize/overlays/disable-clickhouse/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/disable-clickhouse/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/disable-clickhouse/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/disable-clickhouse/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/disable-clickhouse/patch.yaml new file mode 100644 index 00000000..f4f97f33 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/disable-clickhouse/patch.yaml @@ -0,0 +1,8 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + clickhouse: + managedClickhouse: null + externalClickhouse: null diff --git a/hack/testing-manifests/wandb/kustomize/overlays/disable-kafka/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/disable-kafka/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/disable-kafka/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/disable-kafka/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/disable-kafka/patch.yaml new file mode 100644 index 00000000..7a9888f2 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/disable-kafka/patch.yaml @@ -0,0 +1,7 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + kafka: + managedKafka: null diff --git a/hack/testing-manifests/wandb/kustomize/overlays/disable-mysql/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/disable-mysql/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/disable-mysql/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/disable-mysql/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/disable-mysql/patch.yaml new file mode 100644 index 00000000..0cb1830d --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/disable-mysql/patch.yaml @@ -0,0 +1,8 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + mysql: + managedMysql: null + externalMysql: null diff --git a/hack/testing-manifests/wandb/kustomize/overlays/disable-objectstore/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/disable-objectstore/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/disable-objectstore/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/disable-objectstore/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/disable-objectstore/patch.yaml new file mode 100644 index 00000000..9c906e80 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/disable-objectstore/patch.yaml @@ -0,0 +1,8 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + objectStore: + managedObjectStore: null + externalObjectStore: null diff --git a/hack/testing-manifests/wandb/kustomize/overlays/disable-redis/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/disable-redis/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/disable-redis/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/disable-redis/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/disable-redis/patch.yaml new file mode 100644 index 00000000..d53ab216 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/disable-redis/patch.yaml @@ -0,0 +1,8 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + redis: + managedRedis: null + externalRedis: null diff --git a/hack/testing-manifests/wandb/kustomize/overlays/external-clickhouse/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/external-clickhouse/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/external-clickhouse/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/external-clickhouse/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/external-clickhouse/patch.yaml new file mode 100644 index 00000000..4f3dc429 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/external-clickhouse/patch.yaml @@ -0,0 +1,27 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + clickhouse: + managedClickhouse: null + externalClickhouse: + # required + host: + name: external-clickhouse-connection + key: Host + port: + name: external-clickhouse-connection + key: Port + database: + name: external-clickhouse-connection + key: Database + username: + name: external-clickhouse-connection + key: User + password: + name: external-clickhouse-connection + key: Password + URL: + name: external-clickhouse-connection + key: url diff --git a/hack/testing-manifests/wandb/kustomize/overlays/external-mysql/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/external-mysql/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/external-mysql/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/external-mysql/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/external-mysql/patch.yaml new file mode 100644 index 00000000..d8e05b2e --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/external-mysql/patch.yaml @@ -0,0 +1,40 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + mysql: + managedMysql: null + externalMysql: + # required + host: + name: external-mysql-connection + key: Host + port: + name: external-mysql-connection + key: Port + database: + name: external-mysql-connection + key: Database + username: + name: external-mysql-connection + key: Username + password: + name: external-mysql-connection + key: Password + URL: + name: external-mysql-connection + key: url + # optional + # tls: + # name: external-mysql-connection + # key: Tls + # sslCa: + # name: external-mysql-connection + # key: SslCa + # sslCert: + # name: external-mysql-connection + # key: SslCert + # sslKey: + # name: external-mysql-connection + # key: SslKey diff --git a/hack/testing-manifests/wandb/kustomize/overlays/external-objectstore/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/external-objectstore/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/external-objectstore/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/external-objectstore/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/external-objectstore/patch.yaml new file mode 100644 index 00000000..3496654d --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/external-objectstore/patch.yaml @@ -0,0 +1,34 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + objectStore: + managedObjectStore: null + externalObjectStore: + # provider: secret key holding s3 (default), gcs, or azure; omit to default to s3. + provider: + name: external-objectstore-connection + key: Provider + # required + endpoint: + name: external-objectstore-connection + key: Host + port: + name: external-objectstore-connection + key: Port + bucket: + name: external-objectstore-connection + key: Bucket + region: + name: external-objectstore-connection + key: Region + URL: + name: external-objectstore-connection + key: url + accessKey: + name: external-objectstore-connection + key: AccessKey + secretKey: + name: external-objectstore-connection + key: SecretKey diff --git a/hack/testing-manifests/wandb/kustomize/overlays/external-redis/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/external-redis/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/external-redis/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/external-redis/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/external-redis/patch.yaml new file mode 100644 index 00000000..b20b3959 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/external-redis/patch.yaml @@ -0,0 +1,28 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + redis: + managedRedis: null + externalRedis: + # required + host: + name: external-redis-connection + key: Host + port: + name: external-redis-connection + key: Port + URL: + name: external-redis-connection + key: url + # optional + # password: + # name: external-redis-connection + # key: Password + # tls: + # name: external-redis-connection + # key: Tls + # sslCa: + # name: external-redis-connection + # key: SslCa diff --git a/hack/testing-manifests/wandb/kustomize/overlays/license-file/.gitignore b/hack/testing-manifests/wandb/kustomize/overlays/license-file/.gitignore new file mode 100644 index 00000000..09ba20c3 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/license-file/.gitignore @@ -0,0 +1 @@ +/patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/license-file/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/license-file/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/license-file/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/networking-gateway-local/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/networking-gateway-local/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/networking-gateway-local/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/networking-gateway-local/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/networking-gateway-local/patch.yaml new file mode 100644 index 00000000..6bc9b957 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/networking-gateway-local/patch.yaml @@ -0,0 +1,19 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + wandb: + hostname: http://wandb.localhost:8080 + networking: + mode: gateway + gatewayAPI: + listenerName: http + gateway: + managed: true + gatewayClassName: nginx + listeners: + - name: http + port: 80 + protocol: HTTP + hostname: wandb.localhost diff --git a/hack/testing-manifests/wandb/kustomize/overlays/networking-ingress-local/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/networking-ingress-local/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/networking-ingress-local/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/networking-ingress-local/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/networking-ingress-local/patch.yaml new file mode 100644 index 00000000..9dc55d54 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/networking-ingress-local/patch.yaml @@ -0,0 +1,11 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + wandb: + hostname: http://wandb.localhost:8080 + networking: + mode: ingress + ingress: + ingressClassName: nginx diff --git a/hack/testing-manifests/wandb/kustomize/overlays/purge-retention/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/purge-retention/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/purge-retention/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/purge-retention/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/purge-retention/patch.yaml new file mode 100644 index 00000000..a307a971 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/purge-retention/patch.yaml @@ -0,0 +1,7 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + retentionPolicy: + onDelete: purge diff --git a/hack/testing-manifests/wandb/kustomize/overlays/server-version-0.78.0/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/server-version-0.78.0/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/server-version-0.78.0/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/server-version-0.78.0/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/server-version-0.78.0/patch.yaml new file mode 100644 index 00000000..ab6a34a6 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/server-version-0.78.0/patch.yaml @@ -0,0 +1,7 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + wandb: + version: "0.78.0" diff --git a/hack/testing-manifests/wandb/kustomize/overlays/size-micro/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/size-micro/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/size-micro/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/size-micro/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/size-micro/patch.yaml new file mode 100644 index 00000000..e2cfc753 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/size-micro/patch.yaml @@ -0,0 +1,6 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + size: micro diff --git a/hack/testing-manifests/wandb/kustomize/overlays/size-small/kustomization.yaml b/hack/testing-manifests/wandb/kustomize/overlays/size-small/kustomization.yaml new file mode 100644 index 00000000..a33514ab --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/size-small/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component +patches: + - path: patch.yaml diff --git a/hack/testing-manifests/wandb/kustomize/overlays/size-small/patch.yaml b/hack/testing-manifests/wandb/kustomize/overlays/size-small/patch.yaml new file mode 100644 index 00000000..3a532e61 --- /dev/null +++ b/hack/testing-manifests/wandb/kustomize/overlays/size-small/patch.yaml @@ -0,0 +1,6 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb +spec: + size: small diff --git a/hack/testing-manifests/wandb/local-registry.yaml b/hack/testing-manifests/wandb/local-registry.yaml index 96725a5c..3d83daae 100644 --- a/hack/testing-manifests/wandb/local-registry.yaml +++ b/hack/testing-manifests/wandb/local-registry.yaml @@ -20,10 +20,10 @@ spec: global: bucket: provider: "s3" - name: "minio.minio.svc.cluster.local:9000/bucket" + name: "seaweedfs.seaweedfs.svc.cluster.local:8333/bucket" region: "us-east-1" - accessKey: "minio" - secretKey: "minio123" + accessKey: "seaweedfs" + secretKey: "seaweedfs123" app: image: diff --git a/hack/testing-manifests/wandb/default.yaml b/hack/testing-manifests/wandb/wandb-default-v1.yaml similarity index 50% rename from hack/testing-manifests/wandb/default.yaml rename to hack/testing-manifests/wandb/wandb-default-v1.yaml index 0c16dbf9..7582134d 100644 --- a/hack/testing-manifests/wandb/default.yaml +++ b/hack/testing-manifests/wandb/wandb-default-v1.yaml @@ -8,44 +8,46 @@ metadata: app.kubernetes.io/part-of: operator app.kubernetes.io/managed-by: kustomize app.kubernetes.io/created-by: operator - name: wandb-default + name: wandb-default-v1 spec: chart: url: https://charts.wandb.ai name: "operator-wandb" - version: "0.41.3" + version: "0.37.1" values: global: + repositoryPrefix: "us-docker.pkg.dev/wandb-production/public" + imageRegistry: "us-docker.pkg.dev/wandb-production/public" bucket: provider: "s3" - name: "minio.minio.svc.cluster.local:9000/bucket" + name: "seaweedfs.seaweedfs.svc.cluster.local:8333/bucket" region: "us-east-1" - accessKey: "minio" - secretKey: "minio123" + accessKey: "seaweedfs" + secretKey: "seaweedfs123" + glue: + enabled: true + api: + enabled: true + executor: + enabled: true + size: "dev" - app: - resources: - requests: - cpu: "100m" - memory: "128Mi" + executor: + install: true - parquet: - resources: - requests: - cpu: "100m" - memory: "128Mi" + filemeta: + install: true - weave: - resources: - requests: - cpu: "100m" - memory: "128Mi" + frontend: + install: true - console: - resources: - requests: - cpu: "100m" - memory: "128Mi" + nginx: + install: true + additionalServices: + bucket: + host: "http://seaweedfs.seaweedfs.svc.cluster.local:8333/bucket" + headers: + Host: "seaweedfs.seaweedfs.svc.cluster.local:8333" ingress: install: false @@ -69,4 +71,4 @@ spec: install: true settingsMigrationJob: - install: false + install: false \ No newline at end of file diff --git a/hack/testing-manifests/wandb/wandb-dev-v2.yaml b/hack/testing-manifests/wandb/wandb-dev-v2.yaml new file mode 100644 index 00000000..def5c60f --- /dev/null +++ b/hack/testing-manifests/wandb/wandb-dev-v2.yaml @@ -0,0 +1,36 @@ +apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: wandb-dev-v2 +spec: + wandb: + hostname: http://localhost + internalServiceAuth: + enabled: true + features: + proxy: true + manifestRepository: "file:///server-manifest" + version: "0.79.0" + size: dev + retentionPolicy: + onDelete: detach + mysql: + managedMysql: + telemetry: + enabled: true + redis: + managedRedis: + telemetry: + enabled: true + kafka: + managedKafka: + telemetry: + enabled: true + objectStore: + managedObjectStore: + telemetry: + enabled: true + clickhouse: + managedClickhouse: + telemetry: + enabled: true diff --git a/hack/testing-manifests/wandb/wandb-legacy-overrides-v1.yaml b/hack/testing-manifests/wandb/wandb-legacy-overrides-v1.yaml new file mode 100644 index 00000000..ef289118 --- /dev/null +++ b/hack/testing-manifests/wandb/wandb-legacy-overrides-v1.yaml @@ -0,0 +1,71 @@ +--- +# v1 CR exercising the legacyOverrides conversion path: +# - global env/extraEnv (env wins on BOTH_LAYERS) -> legacyOverrides.global +# - per-app env (api, incl. a valueFrom body) and resources (parquet sizing) +# - a renamed section (nginx -> nginx-proxy via the manifest's legacyKey) +# - sections with no v2 application (app, console) that the reconciler +# should log as unmapped and leave unapplied +# - a helm-templated env value the conversion should drop with a log +apiVersion: apps.wandb.com/v1 +kind: WeightsAndBiases +metadata: + labels: + app.kubernetes.io/name: weightsandbiases + app.kubernetes.io/instance: weightsandbiases-sample + app.kubernetes.io/part-of: operator + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: operator + # Keep CR names <=16 chars: managed ClickHouse derives + # chk-<name>-clickhouse-keeper-deploy-confd-default-0-0 volume names that + # must fit the 63-char DNS label limit or the Altinity operator silently + # fails to create the StatefulSets. + name: wandb-legacy-v1 +spec: + chart: + url: https://charts.wandb.ai + name: "operator-wandb" + version: "0.37.1" + values: + global: + size: "dev" + env: + GLOBAL_FROM_ENV: "global-env" + BOTH_LAYERS: "from-env" + extraEnv: + BOTH_LAYERS: "from-extra-env" + HTTP_PROXY: "http://proxy.internal:3128" + NO_PROXY: "10.0.0.0/8,.svc.cluster.local" + TEMPLATED_DROPPED: "{{ .Release.Name }}-value" + + api: + env: + GORILLA_CUSTOM_FLAG: "true" + API_SECRET_VAR: + valueFrom: + secretKeyRef: + name: my-api-secret + key: token + + parquet: + sizing: + dev: + resources: + requests: + cpu: "250m" + memory: "256Mi" + limits: + memory: "1Gi" + + nginx: + env: + NGINX_EXTRA: "1" + + app: + image: + tag: 0.83.0-daily.17 + env: + MONOLITH_ONLY_VAR: "ignored-by-v2" + + console: + env: + CONSOLE_ONLY_VAR: "ignored-by-v2" diff --git a/hack/tilt/wandbcr/main.go b/hack/tilt/wandbcr/main.go new file mode 100644 index 00000000..d7841600 --- /dev/null +++ b/hack/tilt/wandbcr/main.go @@ -0,0 +1,614 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "flag" + "fmt" + "math/big" + "os" + "path/filepath" + "strings" + "time" + + v2 "github.com/wandb/operator/api/v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" +) + +const ( + defaultOutPath = "hack/testing-manifests/wandb/.generated/tilt-wandb-cr.yaml" + defaultName = "wandb" + defaultNamespace = "wandb" + defaultHostname = "http://localhost:8080" + defaultIngressHostname = "http://wandb.localhost:8080" + localManifestRepository = "file:///server-manifest" + defaultManifestSource = "published" + defaultVersion = "0.80.0" + defaultSize = v2.SizeDev + defaultRetentionPolicy = v2.DetachOnDelete + customCAConfigMapName = "wandb-user-ca-certs" + customCAConfigMapKey = "user-ca.crt" + + externalMySQLSecret = "external-mysql-connection" + externalMySQLTLSSecret = "external-mysql-tls" + externalRedisSecret = "external-redis-connection" + externalRedisTLSSecret = "external-redis-tls" + externalObjectStoreSecret = "external-objectstore-connection" +) + +type Options struct { + OutPath string + CRFile string + Name string + Namespace string + Hostname string + Version string + Size string + RetentionPolicy string + LicenseFile string + ManifestSource string + ObservabilityMode string + NetworkMode string + GatewayClass string + IngressClass string + CreateCA bool + CreateCASet bool + IssuerName string + + ExternalMySQL bool + ExternalRedis bool + ExternalObjectStore bool + CustomCA bool + CustomCAConfigMapOut string + CustomCACertificatePEM string +} + +func main() { + opts := Options{} + flag.StringVar(&opts.OutPath, "out", defaultOutPath, "Path to write the generated WeightsAndBiases CR YAML") + flag.StringVar(&opts.CRFile, "cr-file", "", "Optional base WeightsAndBiases CR YAML to patch") + flag.StringVar(&opts.Name, "name", defaultName, "WeightsAndBiases resource name") + flag.StringVar(&opts.Namespace, "namespace", defaultNamespace, "WeightsAndBiases resource namespace") + flag.StringVar(&opts.Hostname, "hostname", defaultHostname, "W&B hostname") + flag.StringVar(&opts.Version, "version", defaultVersion, "W&B version") + flag.StringVar(&opts.Size, "size", string(defaultSize), "W&B size") + flag.StringVar(&opts.RetentionPolicy, "retention-policy", string(defaultRetentionPolicy), "Retention policy on delete") + flag.StringVar(&opts.LicenseFile, "license-file", "", "Path to W&B license file") + flag.StringVar(&opts.ManifestSource, "manifest-source", defaultManifestSource, "Server manifest source: published or local") + flag.StringVar(&opts.ObservabilityMode, "observability-mode", "off", "Observability mode: off, full, or forward") + flag.StringVar(&opts.NetworkMode, "network-mode", "gateway", "Networking mode: gateway or ingress") + flag.StringVar(&opts.GatewayClass, "gateway-class", "nginx", "GatewayClass name for gateway mode") + flag.StringVar(&opts.IngressClass, "ingress-class", "nginx", "IngressClass name for ingress mode") + flag.BoolVar(&opts.CreateCA, "create-ca", true, "Use the generated W&B CA issuer for HTTPS hostnames") + flag.StringVar(&opts.IssuerName, "issuer-name", "", "Existing cert-manager issuer for HTTPS hostnames") + flag.BoolVar(&opts.ExternalMySQL, "external-mysql", false, "Use the test-infra external MySQL connection Secret") + flag.BoolVar(&opts.ExternalRedis, "external-redis", false, "Use the test-infra external Redis connection Secret") + flag.BoolVar(&opts.ExternalObjectStore, "external-objectstore", false, "Use the test-infra external object store connection Secret") + flag.BoolVar(&opts.CustomCA, "custom-ca", false, "Generate and configure custom CA material") + flag.StringVar(&opts.CustomCAConfigMapOut, "custom-ca-configmap-out", "", "Path to write the generated custom CA ConfigMap YAML") + flag.Parse() + flag.Visit(func(f *flag.Flag) { + if f.Name == "create-ca" { + opts.CreateCASet = true + } + }) + + if err := Run(opts); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func Run(opts Options) error { + applyDefaults(&opts) + + cr, configMap, err := BuildArtifacts(opts) + if err != nil { + return err + } + + data, err := marshalCRYAML(cr) + if err != nil { + return fmt.Errorf("marshal CR YAML: %w", err) + } + + if err := os.MkdirAll(filepath.Dir(opts.OutPath), 0o755); err != nil { + return fmt.Errorf("create generated directory: %w", err) + } + if err := os.WriteFile(opts.OutPath, data, 0o644); err != nil { + return fmt.Errorf("write generated CR: %w", err) + } + if configMap != nil && opts.CustomCAConfigMapOut != "" { + data, err := marshalObjectYAML(configMap) + if err != nil { + return fmt.Errorf("marshal custom CA ConfigMap YAML: %w", err) + } + if err := os.MkdirAll(filepath.Dir(opts.CustomCAConfigMapOut), 0o755); err != nil { + return fmt.Errorf("create custom CA ConfigMap directory: %w", err) + } + if err := os.WriteFile(opts.CustomCAConfigMapOut, data, 0o644); err != nil { + return fmt.Errorf("write custom CA ConfigMap: %w", err) + } + } + return nil +} + +func marshalCRYAML(cr *v2.WeightsAndBiases) ([]byte, error) { + obj, err := prunedObject(cr) + if err != nil { + return nil, err + } + delete(obj, "status") + return yaml.Marshal(obj) +} + +func marshalObjectYAML(value interface{}) ([]byte, error) { + obj, err := prunedObject(value) + if err != nil { + return nil, err + } + return yaml.Marshal(obj) +} + +func prunedObject(value interface{}) (map[string]interface{}, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + + obj := map[string]interface{}{} + if err := json.Unmarshal(data, &obj); err != nil { + return nil, err + } + + if _, keep := pruneEmpty(obj); !keep { + return map[string]interface{}{}, nil + } + return obj, nil +} + +func pruneEmpty(value interface{}) (interface{}, bool) { + switch typed := value.(type) { + case nil: + return nil, false + case string: + return typed, typed != "" + case []interface{}: + items := []interface{}{} + for _, item := range typed { + if pruned, keep := pruneEmpty(item); keep { + items = append(items, pruned) + } + } + return items, len(items) > 0 + case map[string]interface{}: + for key, item := range typed { + if pruned, keep := pruneEmpty(item); keep { + typed[key] = pruned + } else { + delete(typed, key) + } + } + return typed, len(typed) > 0 + default: + return typed, true + } +} + +func BuildCR(opts Options) (*v2.WeightsAndBiases, error) { + cr, _, err := BuildArtifacts(opts) + return cr, err +} + +func BuildArtifacts(opts Options) (*v2.WeightsAndBiases, *corev1.ConfigMap, error) { + applyDefaults(&opts) + + if opts.CustomCA && opts.CustomCACertificatePEM == "" { + certPEM, err := generateSelfSignedCACertificate() + if err != nil { + return nil, nil, err + } + opts.CustomCACertificatePEM = certPEM + } + + cr, err := baseCR(opts.CRFile) + if err != nil { + return nil, nil, err + } + + ensureTypeMeta(cr) + patchMetadata(cr, opts) + patchScalarSpec(cr, opts) + if err := patchManifestRepository(cr, opts.ManifestSource); err != nil { + return nil, nil, err + } + if err := patchLicense(cr, opts.LicenseFile); err != nil { + return nil, nil, err + } + if err := patchNetworking(cr, opts); err != nil { + return nil, nil, err + } + if err := patchTelemetry(cr, opts.ObservabilityMode); err != nil { + return nil, nil, err + } + patchExternalInfra(cr, opts) + + var configMap *corev1.ConfigMap + if opts.CustomCA { + patchCustomCA(cr, opts.CustomCACertificatePEM) + configMap = customCAConfigMap(cr.Namespace, opts.CustomCACertificatePEM) + } + + return cr, configMap, nil +} + +func applyDefaults(opts *Options) { + if opts.OutPath == "" { + opts.OutPath = defaultOutPath + } + if opts.Name == "" { + opts.Name = defaultName + } + if opts.Namespace == "" { + opts.Namespace = defaultNamespace + } + if opts.Hostname == "" { + opts.Hostname = defaultHostname + } + if opts.Version == "" { + opts.Version = defaultVersion + } + if opts.Size == "" { + opts.Size = string(defaultSize) + } + if opts.RetentionPolicy == "" { + opts.RetentionPolicy = string(defaultRetentionPolicy) + } + if opts.ManifestSource == "" { + opts.ManifestSource = defaultManifestSource + } + if opts.ObservabilityMode == "" { + opts.ObservabilityMode = "off" + } + if opts.NetworkMode == "" { + opts.NetworkMode = "gateway" + } + if opts.GatewayClass == "" { + opts.GatewayClass = "nginx" + } + if opts.IngressClass == "" { + opts.IngressClass = "nginx" + } + if !opts.CreateCASet { + opts.CreateCA = true + } +} + +func baseCR(crFile string) (*v2.WeightsAndBiases, error) { + if crFile != "" { + data, err := os.ReadFile(crFile) + if err != nil { + return nil, fmt.Errorf("read cr-file: %w", err) + } + + cr := &v2.WeightsAndBiases{} + if err := yaml.Unmarshal(data, cr); err != nil { + return nil, fmt.Errorf("parse cr-file: %w", err) + } + return cr, nil + } + + return &v2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps.wandb.com/v2", + Kind: "WeightsAndBiases", + }, + Spec: v2.WeightsAndBiasesSpec{ + Wandb: v2.WandbAppSpec{ + Features: map[string]bool{"proxy": true}, + InternalServiceAuth: v2.InternalServiceAuth{Enabled: boolPtr(false)}, + }, + MySQL: map[string]v2.MySQLSpec{v2.DefaultInstanceName: {ManagedMysql: &v2.ManagedMysqlSpec{}}}, + Redis: map[string]v2.RedisSpec{v2.DefaultInstanceName: {ManagedRedis: &v2.ManagedRedisSpec{}}}, + Kafka: v2.KafkaSpec{ManagedKafka: &v2.ManagedKafkaSpec{}}, + ObjectStore: map[string]v2.ObjectStoreSpec{v2.DefaultInstanceName: {ManagedObjectStore: &v2.ManagedObjectStoreSpec{}}}, + ClickHouse: map[string]v2.ClickHouseSpec{v2.DefaultInstanceName: {ManagedClickHouse: &v2.ManagedClickHouseSpec{}}}, + }, + }, nil +} + +func ensureTypeMeta(cr *v2.WeightsAndBiases) { + if cr.APIVersion == "" { + cr.APIVersion = "apps.wandb.com/v2" + } + if cr.Kind == "" { + cr.Kind = "WeightsAndBiases" + } +} + +func patchMetadata(cr *v2.WeightsAndBiases, opts Options) { + cr.Name = opts.Name + cr.Namespace = opts.Namespace + if cr.Labels == nil { + cr.Labels = map[string]string{} + } + cr.Labels["app.kubernetes.io/managed-by"] = "tilt" +} + +func patchScalarSpec(cr *v2.WeightsAndBiases, opts Options) { + cr.Spec.Wandb.Hostname = effectiveHostname(opts) + cr.Spec.Wandb.Version = opts.Version + if cr.Spec.Wandb.Features == nil { + cr.Spec.Wandb.Features = map[string]bool{} + } + cr.Spec.Wandb.Features["proxy"] = true + cr.Spec.Wandb.InternalServiceAuth = v2.InternalServiceAuth{Enabled: boolPtr(false)} + cr.Spec.Size = v2.Size(opts.Size) + cr.Spec.RetentionPolicy.OnDelete = v2.OnDeletePolicy(opts.RetentionPolicy) +} + +func patchManifestRepository(cr *v2.WeightsAndBiases, manifestSource string) error { + switch normalizeManifestSource(manifestSource) { + case "published": + cr.Spec.Wandb.ManifestRepository = "" + case "local": + cr.Spec.Wandb.ManifestRepository = localManifestRepository + default: + return fmt.Errorf("manifest-source must be one of: published, local") + } + return nil +} + +func patchLicense(cr *v2.WeightsAndBiases, licenseFile string) error { + if licenseFile == "" { + return nil + } + + data, err := os.ReadFile(licenseFile) + if err != nil { + return fmt.Errorf("read license-file: %w", err) + } + cr.Spec.Wandb.License = strings.TrimSpace(string(data)) + return nil +} + +func patchNetworking(cr *v2.WeightsAndBiases, opts Options) error { + mode := normalizeNetworkMode(opts.NetworkMode) + switch mode { + case "gateway": + if opts.GatewayClass == "" { + return fmt.Errorf("gateway network mode requires gateway-class") + } + cr.Spec.Networking.Mode = v2.NetworkingModeGatewayAPI + cr.Spec.Networking.Ingress = nil + cr.Spec.Networking.GatewayAPI = &v2.GatewayAPIConfig{ + Gateway: v2.GatewayConfig{ + Managed: true, + GatewayClassName: stringPtr(opts.GatewayClass), + }, + } + case "ingress": + if opts.IngressClass == "" { + return fmt.Errorf("ingress network mode requires ingress-class") + } + cr.Spec.Networking.Mode = v2.NetworkingModeIngress + cr.Spec.Networking.GatewayAPI = nil + cr.Spec.Networking.Ingress = &v2.IngressConfig{ + IngressClassName: stringPtr(opts.IngressClass), + } + default: + return fmt.Errorf("network-mode must be one of: gateway, ingress") + } + + if strings.HasPrefix(cr.Spec.Wandb.Hostname, "https://") { + if opts.CreateCA { + cr.Spec.Networking.TLS = &v2.TLSConfig{ + SecretName: opts.Name + "-tls-secret", + CertManager: &v2.CertManagerConfig{ + Issuer: opts.Name + "-ca-issuer", + }, + } + } else if opts.IssuerName != "" { + cr.Spec.Networking.TLS = &v2.TLSConfig{ + SecretName: opts.Name + "-tls-secret", + CertManager: &v2.CertManagerConfig{ + Issuer: opts.IssuerName, + }, + } + } else { + return fmt.Errorf("https hostname requires create-ca=true or issuer-name") + } + } else { + cr.Spec.Networking.TLS = nil + } + + return nil +} + +func patchTelemetry(cr *v2.WeightsAndBiases, observabilityMode string) error { + mode := normalizeObservabilityMode(observabilityMode) + var enabled bool + switch mode { + case "off": + enabled = false + case "full", "forward": + enabled = true + default: + return fmt.Errorf("observability-mode must be one of: off, full, forward") + } + + for _, spec := range cr.Spec.MySQL { + if spec.ManagedMysql != nil { + spec.ManagedMysql.Telemetry.Enabled = enabled + } + } + for _, spec := range cr.Spec.Redis { + if spec.ManagedRedis != nil { + spec.ManagedRedis.Telemetry.Enabled = enabled + } + } + if cr.Spec.Kafka.ManagedKafka != nil { + cr.Spec.Kafka.ManagedKafka.Telemetry.Enabled = enabled + } + for _, spec := range cr.Spec.ObjectStore { + if spec.ManagedObjectStore != nil { + spec.ManagedObjectStore.Telemetry.Enabled = enabled + } + } + for _, spec := range cr.Spec.ClickHouse { + if spec.ManagedClickHouse != nil { + spec.ManagedClickHouse.Telemetry.Enabled = enabled + } + } + return nil +} + +func patchExternalInfra(cr *v2.WeightsAndBiases, opts Options) { + if opts.ExternalMySQL { + conn := &v2.MysqlConnection{ + Host: secretKeySelector(externalMySQLSecret, "Host"), + Port: secretKeySelector(externalMySQLSecret, "Port"), + Database: secretKeySelector(externalMySQLSecret, "Database"), + Username: secretKeySelector(externalMySQLSecret, "Username"), + Password: secretKeySelector(externalMySQLSecret, "Password"), + } + if opts.CustomCA { + conn.SslCa = secretKeySelector(externalMySQLTLSSecret, "ca.crt") + } + cr.Spec.MySQL[v2.DefaultInstanceName] = v2.MySQLSpec{ExternalMysql: conn} + } + + if opts.ExternalRedis { + conn := &v2.RedisConnection{ + Host: secretKeySelector(externalRedisSecret, "Host"), + Port: secretKeySelector(externalRedisSecret, "Port"), + } + if opts.CustomCA { + conn.SslCa = secretKeySelector(externalRedisTLSSecret, "ca.crt") + } + cr.Spec.Redis[v2.DefaultInstanceName] = v2.RedisSpec{ExternalRedis: conn} + } + + if opts.ExternalObjectStore { + cr.Spec.ObjectStore[v2.DefaultInstanceName] = v2.ObjectStoreSpec{ExternalObjectStore: &v2.ObjectStoreConnection{ + Provider: secretKeySelector(externalObjectStoreSecret, "Provider"), + Endpoint: secretKeySelector(externalObjectStoreSecret, "Host"), + Port: secretKeySelector(externalObjectStoreSecret, "Port"), + Bucket: secretKeySelector(externalObjectStoreSecret, "Bucket"), + Region: secretKeySelector(externalObjectStoreSecret, "Region"), + AccessKey: secretKeySelector(externalObjectStoreSecret, "AccessKey"), + SecretKey: secretKeySelector(externalObjectStoreSecret, "SecretKey"), + }} + } +} + +func patchCustomCA(cr *v2.WeightsAndBiases, certPEM string) { + cr.Spec.Global.CustomCACerts = []string{certPEM} + cr.Spec.Global.CACertsConfigMap = customCAConfigMapName +} + +func customCAConfigMap(namespace, certPEM string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: customCAConfigMapName, + Namespace: namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "tilt", + "app.kubernetes.io/part-of": "wandb", + }, + }, + Data: map[string]string{ + customCAConfigMapKey: certPEM, + }, + } +} + +func generateSelfSignedCACertificate() (string, error) { + serialLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialLimit) + if err != nil { + return "", fmt.Errorf("generate custom CA serial: %w", err) + } + + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return "", fmt.Errorf("generate custom CA key: %w", err) + } + + now := time.Now() + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "wandb-tilt-custom-ca", + }, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + IsCA: true, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + return "", fmt.Errorf("generate custom CA certificate: %w", err) + } + + return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})), nil +} + +func effectiveHostname(opts Options) string { + if normalizeNetworkMode(opts.NetworkMode) == "ingress" && opts.Hostname == defaultHostname { + return defaultIngressHostname + } + return opts.Hostname +} + +func normalizeNetworkMode(mode string) string { + switch strings.ToLower(mode) { + case "gateway": + return "gateway" + case "ingress": + return "ingress" + default: + return mode + } +} + +func normalizeObservabilityMode(mode string) string { + if mode == "" { + return "off" + } + return strings.ToLower(mode) +} + +func normalizeManifestSource(source string) string { + if source == "" { + return defaultManifestSource + } + return strings.ToLower(source) +} + +func boolPtr(value bool) *bool { + return &value +} + +func stringPtr(value string) *string { + return &value +} + +func secretKeySelector(secretName, key string) corev1.SecretKeySelector { + return corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + Key: key, + } +} diff --git a/hack/tilt/wandbcr/main_test.go b/hack/tilt/wandbcr/main_test.go new file mode 100644 index 00000000..9787400f --- /dev/null +++ b/hack/tilt/wandbcr/main_test.go @@ -0,0 +1,499 @@ +package main + +import ( + "crypto/x509" + "encoding/pem" + "os" + "path/filepath" + "strings" + "testing" + + v2 "github.com/wandb/operator/api/v2" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +const testCustomCAPEM = `-----BEGIN CERTIFICATE----- +MIIBtest +-----END CERTIFICATE----- +` + +func TestBuildCRDefaultGateway(t *testing.T) { + cr, err := BuildCR(Options{}) + if err != nil { + t.Fatal(err) + } + + if cr.APIVersion != "apps.wandb.com/v2" || cr.Kind != "WeightsAndBiases" { + t.Fatalf("unexpected type meta: %s %s", cr.APIVersion, cr.Kind) + } + if cr.Name != defaultName || cr.Namespace != defaultNamespace { + t.Fatalf("unexpected metadata: %s/%s", cr.Namespace, cr.Name) + } + if cr.Spec.Wandb.ManifestRepository != "" { + t.Fatalf("manifest repository = %q", cr.Spec.Wandb.ManifestRepository) + } + if cr.Spec.Wandb.Version != defaultVersion { + t.Fatalf("version = %q", cr.Spec.Wandb.Version) + } + if !cr.Spec.Wandb.Features["proxy"] { + t.Fatalf("proxy feature not enabled") + } + if cr.Spec.Wandb.InternalServiceAuth.Enabled == nil || *cr.Spec.Wandb.InternalServiceAuth.Enabled { + t.Fatalf("internal service auth should be explicitly disabled") + } + if cr.Spec.MySQL[v2.DefaultInstanceName].ManagedMysql == nil || cr.Spec.MySQL[v2.DefaultInstanceName].ManagedMysql.Telemetry.Enabled { + t.Fatalf("mysql telemetry should be disabled by default") + } + if cr.Spec.Redis[v2.DefaultInstanceName].ManagedRedis == nil || cr.Spec.Redis[v2.DefaultInstanceName].ManagedRedis.Telemetry.Enabled { + t.Fatalf("redis telemetry should be disabled by default") + } + if cr.Spec.Kafka.ManagedKafka == nil || cr.Spec.Kafka.ManagedKafka.Telemetry.Enabled { + t.Fatalf("kafka telemetry should be disabled by default") + } + if cr.Spec.ObjectStore[v2.DefaultInstanceName].ManagedObjectStore == nil || cr.Spec.ObjectStore[v2.DefaultInstanceName].ManagedObjectStore.Telemetry.Enabled { + t.Fatalf("object store telemetry should be disabled by default") + } + if cr.Spec.ClickHouse[v2.DefaultInstanceName].ManagedClickHouse == nil || cr.Spec.ClickHouse[v2.DefaultInstanceName].ManagedClickHouse.Telemetry.Enabled { + t.Fatalf("clickhouse telemetry should be disabled by default") + } + if cr.Spec.MySQL[v2.DefaultInstanceName].ExternalMysql != nil { + t.Fatalf("external mysql should be unset by default") + } + if cr.Spec.Redis[v2.DefaultInstanceName].ExternalRedis != nil { + t.Fatalf("external redis should be unset by default") + } + if cr.Spec.ObjectStore[v2.DefaultInstanceName].ExternalObjectStore != nil { + t.Fatalf("external object store should be unset by default") + } + if len(cr.Spec.Global.CustomCACerts) != 0 || cr.Spec.Global.CACertsConfigMap != "" { + t.Fatalf("custom CA fields should be unset by default: %#v", cr.Spec.Global) + } + if cr.Spec.Networking.Mode != v2.NetworkingModeGatewayAPI { + t.Fatalf("networking mode = %q", cr.Spec.Networking.Mode) + } + if cr.Spec.Networking.GatewayAPI == nil || cr.Spec.Networking.GatewayAPI.Gateway.GatewayClassName == nil || *cr.Spec.Networking.GatewayAPI.Gateway.GatewayClassName != "nginx" { + t.Fatalf("gateway class was not set") + } +} + +func TestBuildCRExternalMySQLOnly(t *testing.T) { + cr, err := BuildCR(Options{ExternalMySQL: true}) + if err != nil { + t.Fatal(err) + } + + if cr.Spec.MySQL[v2.DefaultInstanceName].ManagedMysql != nil { + t.Fatalf("managed mysql should be disabled") + } + if cr.Spec.MySQL[v2.DefaultInstanceName].ExternalMysql == nil { + t.Fatalf("external mysql should be configured") + } + assertSelector(t, cr.Spec.MySQL[v2.DefaultInstanceName].ExternalMysql.Host, externalMySQLSecret, "Host") + assertSelector(t, cr.Spec.MySQL[v2.DefaultInstanceName].ExternalMysql.Port, externalMySQLSecret, "Port") + assertSelector(t, cr.Spec.MySQL[v2.DefaultInstanceName].ExternalMysql.Database, externalMySQLSecret, "Database") + assertSelector(t, cr.Spec.MySQL[v2.DefaultInstanceName].ExternalMysql.Username, externalMySQLSecret, "Username") + assertSelector(t, cr.Spec.MySQL[v2.DefaultInstanceName].ExternalMysql.Password, externalMySQLSecret, "Password") + assertEmptySelector(t, cr.Spec.MySQL[v2.DefaultInstanceName].ExternalMysql.SslCa, "mysql sslCa") + + if cr.Spec.Redis[v2.DefaultInstanceName].ManagedRedis == nil || cr.Spec.Redis[v2.DefaultInstanceName].ExternalRedis != nil { + t.Fatalf("redis should remain managed") + } + if cr.Spec.ObjectStore[v2.DefaultInstanceName].ManagedObjectStore == nil || cr.Spec.ObjectStore[v2.DefaultInstanceName].ExternalObjectStore != nil { + t.Fatalf("object store should remain managed") + } +} + +func TestBuildCRExternalRedisOnly(t *testing.T) { + cr, err := BuildCR(Options{ExternalRedis: true}) + if err != nil { + t.Fatal(err) + } + + if cr.Spec.Redis[v2.DefaultInstanceName].ManagedRedis != nil { + t.Fatalf("managed redis should be disabled") + } + if cr.Spec.Redis[v2.DefaultInstanceName].ExternalRedis == nil { + t.Fatalf("external redis should be configured") + } + assertSelector(t, cr.Spec.Redis[v2.DefaultInstanceName].ExternalRedis.Host, externalRedisSecret, "Host") + assertSelector(t, cr.Spec.Redis[v2.DefaultInstanceName].ExternalRedis.Port, externalRedisSecret, "Port") + assertEmptySelector(t, cr.Spec.Redis[v2.DefaultInstanceName].ExternalRedis.Password, "redis password") + assertEmptySelector(t, cr.Spec.Redis[v2.DefaultInstanceName].ExternalRedis.SslCa, "redis sslCa") + + if cr.Spec.MySQL[v2.DefaultInstanceName].ManagedMysql == nil || cr.Spec.MySQL[v2.DefaultInstanceName].ExternalMysql != nil { + t.Fatalf("mysql should remain managed") + } + if cr.Spec.ObjectStore[v2.DefaultInstanceName].ManagedObjectStore == nil || cr.Spec.ObjectStore[v2.DefaultInstanceName].ExternalObjectStore != nil { + t.Fatalf("object store should remain managed") + } +} + +func TestBuildCRExternalObjectStoreOnly(t *testing.T) { + cr, err := BuildCR(Options{ExternalObjectStore: true}) + if err != nil { + t.Fatal(err) + } + + if cr.Spec.ObjectStore[v2.DefaultInstanceName].ManagedObjectStore != nil { + t.Fatalf("managed object store should be disabled") + } + if cr.Spec.ObjectStore[v2.DefaultInstanceName].ExternalObjectStore == nil { + t.Fatalf("external object store should be configured") + } + assertSelector(t, cr.Spec.ObjectStore[v2.DefaultInstanceName].ExternalObjectStore.Provider, externalObjectStoreSecret, "Provider") + assertSelector(t, cr.Spec.ObjectStore[v2.DefaultInstanceName].ExternalObjectStore.Endpoint, externalObjectStoreSecret, "Host") + assertSelector(t, cr.Spec.ObjectStore[v2.DefaultInstanceName].ExternalObjectStore.Port, externalObjectStoreSecret, "Port") + assertSelector(t, cr.Spec.ObjectStore[v2.DefaultInstanceName].ExternalObjectStore.Bucket, externalObjectStoreSecret, "Bucket") + assertSelector(t, cr.Spec.ObjectStore[v2.DefaultInstanceName].ExternalObjectStore.Region, externalObjectStoreSecret, "Region") + assertSelector(t, cr.Spec.ObjectStore[v2.DefaultInstanceName].ExternalObjectStore.AccessKey, externalObjectStoreSecret, "AccessKey") + assertSelector(t, cr.Spec.ObjectStore[v2.DefaultInstanceName].ExternalObjectStore.SecretKey, externalObjectStoreSecret, "SecretKey") + + if cr.Spec.MySQL[v2.DefaultInstanceName].ManagedMysql == nil || cr.Spec.MySQL[v2.DefaultInstanceName].ExternalMysql != nil { + t.Fatalf("mysql should remain managed") + } + if cr.Spec.Redis[v2.DefaultInstanceName].ManagedRedis == nil || cr.Spec.Redis[v2.DefaultInstanceName].ExternalRedis != nil { + t.Fatalf("redis should remain managed") + } +} + +func TestBuildArtifactsCustomCAOnly(t *testing.T) { + cr, configMap, err := BuildArtifacts(Options{ + CustomCA: true, + CustomCACertificatePEM: testCustomCAPEM, + }) + if err != nil { + t.Fatal(err) + } + + if len(cr.Spec.Global.CustomCACerts) != 1 || cr.Spec.Global.CustomCACerts[0] != testCustomCAPEM { + t.Fatalf("custom CA certs not configured: %#v", cr.Spec.Global.CustomCACerts) + } + if cr.Spec.Global.CACertsConfigMap != customCAConfigMapName { + t.Fatalf("caCertsConfigMap = %q", cr.Spec.Global.CACertsConfigMap) + } + if configMap == nil { + t.Fatalf("custom CA ConfigMap should be generated") + } + if configMap.Name != customCAConfigMapName || configMap.Namespace != defaultNamespace { + t.Fatalf("unexpected ConfigMap metadata: %s/%s", configMap.Namespace, configMap.Name) + } + if configMap.Data[customCAConfigMapKey] != testCustomCAPEM { + t.Fatalf("ConfigMap cert data not populated") + } + if cr.Spec.MySQL[v2.DefaultInstanceName].ExternalMysql != nil || cr.Spec.Redis[v2.DefaultInstanceName].ExternalRedis != nil || cr.Spec.ObjectStore[v2.DefaultInstanceName].ExternalObjectStore != nil { + t.Fatalf("custom CA should not switch infra to external by itself") + } +} + +func TestBuildArtifactsGeneratesValidCustomCA(t *testing.T) { + cr, configMap, err := BuildArtifacts(Options{CustomCA: true}) + if err != nil { + t.Fatal(err) + } + + if len(cr.Spec.Global.CustomCACerts) != 1 { + t.Fatalf("expected one generated custom CA, got %d", len(cr.Spec.Global.CustomCACerts)) + } + if configMap == nil || configMap.Data[customCAConfigMapKey] != cr.Spec.Global.CustomCACerts[0] { + t.Fatalf("generated ConfigMap should contain the same CA cert as the CR") + } + + block, _ := pem.Decode([]byte(cr.Spec.Global.CustomCACerts[0])) + if block == nil { + t.Fatalf("generated custom CA is not PEM encoded") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatal(err) + } + if !cert.IsCA { + t.Fatalf("generated certificate is not a CA") + } +} + +func TestBuildArtifactsExternalInfraWithCustomCA(t *testing.T) { + cr, configMap, err := BuildArtifacts(Options{ + ExternalMySQL: true, + ExternalRedis: true, + ExternalObjectStore: true, + CustomCA: true, + CustomCACertificatePEM: testCustomCAPEM, + }) + if err != nil { + t.Fatal(err) + } + + if configMap == nil { + t.Fatalf("custom CA ConfigMap should be generated") + } + assertSelector(t, cr.Spec.MySQL[v2.DefaultInstanceName].ExternalMysql.SslCa, externalMySQLTLSSecret, "ca.crt") + assertSelector(t, cr.Spec.Redis[v2.DefaultInstanceName].ExternalRedis.SslCa, externalRedisTLSSecret, "ca.crt") + if cr.Spec.ObjectStore[v2.DefaultInstanceName].ExternalObjectStore == nil { + t.Fatalf("external object store should be configured") + } + if cr.Spec.ObjectStore[v2.DefaultInstanceName].ManagedObjectStore != nil || cr.Spec.MySQL[v2.DefaultInstanceName].ManagedMysql != nil || cr.Spec.Redis[v2.DefaultInstanceName].ManagedRedis != nil { + t.Fatalf("selected external infra should disable corresponding managed infra") + } +} + +func TestBuildCRLocalManifestSource(t *testing.T) { + cr, err := BuildCR(Options{ManifestSource: "local"}) + if err != nil { + t.Fatal(err) + } + + if cr.Spec.Wandb.ManifestRepository != localManifestRepository { + t.Fatalf("manifest repository = %q", cr.Spec.Wandb.ManifestRepository) + } +} + +func TestBuildCRInvalidManifestSourceReturnsError(t *testing.T) { + _, err := BuildCR(Options{ManifestSource: "testing-manifests"}) + if err == nil { + t.Fatal("expected manifest source error") + } + if !strings.Contains(err.Error(), "manifest-source") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestBuildCRIngressRewritesDefaultHostname(t *testing.T) { + cr, err := BuildCR(Options{NetworkMode: "ingress"}) + if err != nil { + t.Fatal(err) + } + + if cr.Spec.Wandb.Hostname != defaultIngressHostname { + t.Fatalf("hostname = %q", cr.Spec.Wandb.Hostname) + } + if cr.Spec.Networking.Mode != v2.NetworkingModeIngress { + t.Fatalf("networking mode = %q", cr.Spec.Networking.Mode) + } + if cr.Spec.Networking.Ingress == nil || cr.Spec.Networking.Ingress.IngressClassName == nil || *cr.Spec.Networking.Ingress.IngressClassName != "nginx" { + t.Fatalf("ingress class was not set") + } + if cr.Spec.Networking.GatewayAPI != nil { + t.Fatalf("gateway config should be cleared in ingress mode") + } +} + +func TestBuildCRInvalidNetworkModeReturnsError(t *testing.T) { + _, err := BuildCR(Options{NetworkMode: "networking-ingress-local"}) + if err == nil { + t.Fatal("expected network mode error") + } + if !strings.Contains(err.Error(), "network-mode") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestBuildCRFullObservabilityEnablesManagedTelemetry(t *testing.T) { + cr, err := BuildCR(Options{ObservabilityMode: "full"}) + if err != nil { + t.Fatal(err) + } + + if !cr.Spec.MySQL[v2.DefaultInstanceName].ManagedMysql.Telemetry.Enabled || + !cr.Spec.Redis[v2.DefaultInstanceName].ManagedRedis.Telemetry.Enabled || + !cr.Spec.Kafka.ManagedKafka.Telemetry.Enabled || + !cr.Spec.ObjectStore[v2.DefaultInstanceName].ManagedObjectStore.Telemetry.Enabled || + !cr.Spec.ClickHouse[v2.DefaultInstanceName].ManagedClickHouse.Telemetry.Enabled { + t.Fatalf("managed telemetry was not enabled") + } +} + +func TestBuildCRInvalidObservabilityModeReturnsError(t *testing.T) { + _, err := BuildCR(Options{ObservabilityMode: "on"}) + if err == nil { + t.Fatal("expected observability mode error") + } + if !strings.Contains(err.Error(), "observability-mode") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestBuildCRPatchesBaseCRAndPreservesUnrelatedFields(t *testing.T) { + dir := t.TempDir() + crFile := filepath.Join(dir, "base.yaml") + base := `apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: custom + namespace: custom-ns + labels: + keep: me +spec: + requireLimits: true + wandb: + hostname: http://old.example + manifestRepository: file:///old-manifest + version: old + additionalHostnames: + - extra.example + networking: + mode: ingress + ingress: + ingressClassName: old +` + if err := os.WriteFile(crFile, []byte(base), 0o644); err != nil { + t.Fatal(err) + } + + cr, err := BuildCR(Options{ + CRFile: crFile, + Name: "patched", + Namespace: "patched-ns", + Hostname: "http://new.example", + Version: "1.2.3", + Size: "micro", + RetentionPolicy: "purge", + NetworkMode: "gateway", + GatewayClass: "custom-gateway", + }) + if err != nil { + t.Fatal(err) + } + + if cr.Name != "patched" || cr.Namespace != "patched-ns" { + t.Fatalf("metadata was not patched: %s/%s", cr.Namespace, cr.Name) + } + if cr.Labels["keep"] != "me" || cr.Labels["app.kubernetes.io/managed-by"] != "tilt" { + t.Fatalf("labels not preserved/patched: %#v", cr.Labels) + } + if !cr.Spec.RequireLimits { + t.Fatalf("unrelated spec field was not preserved") + } + if len(cr.Spec.Wandb.AdditionalHostnames) != 1 || cr.Spec.Wandb.AdditionalHostnames[0] != "extra.example" { + t.Fatalf("additional hostnames were not preserved: %#v", cr.Spec.Wandb.AdditionalHostnames) + } + if cr.Spec.Wandb.Hostname != "http://new.example" || cr.Spec.Wandb.Version != "1.2.3" { + t.Fatalf("wandb fields not patched: %#v", cr.Spec.Wandb) + } + if cr.Spec.Wandb.ManifestRepository != "" { + t.Fatalf("manifest repository should be cleared for published source: %q", cr.Spec.Wandb.ManifestRepository) + } + if cr.Spec.Size != v2.SizeMicro || cr.Spec.RetentionPolicy.OnDelete != v2.PurgeOnDelete { + t.Fatalf("size/retention not patched: %q %q", cr.Spec.Size, cr.Spec.RetentionPolicy.OnDelete) + } + if cr.Spec.Networking.Mode != v2.NetworkingModeGatewayAPI || cr.Spec.Networking.Ingress != nil { + t.Fatalf("networking not patched to gateway: %#v", cr.Spec.Networking) + } +} + +func TestBuildCRUnreadableLicenseFileReturnsError(t *testing.T) { + _, err := BuildCR(Options{LicenseFile: filepath.Join(t.TempDir(), "missing-license")}) + if err == nil { + t.Fatal("expected license file error") + } +} + +func TestBuildCRHTTPSUsesGeneratedCAIssuer(t *testing.T) { + cr, err := BuildCR(Options{Hostname: "https://wandb.example"}) + if err != nil { + t.Fatal(err) + } + + if cr.Spec.Networking.TLS == nil || cr.Spec.Networking.TLS.CertManager == nil { + t.Fatalf("TLS config was not set") + } + if cr.Spec.Networking.TLS.SecretName != "wandb-tls-secret" { + t.Fatalf("secret name = %q", cr.Spec.Networking.TLS.SecretName) + } + if cr.Spec.Networking.TLS.CertManager.Issuer != "wandb-ca-issuer" { + t.Fatalf("issuer = %q", cr.Spec.Networking.TLS.CertManager.Issuer) + } +} + +func TestBuildCRHTTPSUsesExplicitIssuer(t *testing.T) { + cr, err := BuildCR(Options{ + Name: "custom", + Hostname: "https://wandb.example", + CreateCA: false, + CreateCASet: true, + IssuerName: "existing-issuer", + }) + if err != nil { + t.Fatal(err) + } + + if cr.Spec.Networking.TLS == nil || cr.Spec.Networking.TLS.CertManager == nil { + t.Fatalf("TLS config was not set") + } + if cr.Spec.Networking.TLS.SecretName != "custom-tls-secret" { + t.Fatalf("secret name = %q", cr.Spec.Networking.TLS.SecretName) + } + if cr.Spec.Networking.TLS.CertManager.Issuer != "existing-issuer" { + t.Fatalf("issuer = %q", cr.Spec.Networking.TLS.CertManager.Issuer) + } +} + +func TestRunWritesStableYAML(t *testing.T) { + out := filepath.Join(t.TempDir(), "generated", "wandb.yaml") + if err := Run(Options{OutPath: out}); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(out) + if err != nil { + t.Fatal(err) + } + + var cr v2.WeightsAndBiases + if err := yaml.Unmarshal(data, &cr); err != nil { + t.Fatal(err) + } + if cr.Name != defaultName || cr.Spec.Networking.Mode != v2.NetworkingModeGatewayAPI { + t.Fatalf("unexpected generated CR: %s %#v", cr.Name, cr.Spec.Networking) + } + rendered := string(data) + if strings.Contains(rendered, "\nstatus:") || strings.Contains(rendered, "\noidc:") || strings.Contains(rendered, "manifestRepository") { + t.Fatalf("generated CR contains empty runtime/defaulted fields:\n%s", rendered) + } +} + +func TestRunWritesCustomCAConfigMapYAML(t *testing.T) { + dir := t.TempDir() + out := filepath.Join(dir, "generated", "wandb.yaml") + configMapOut := filepath.Join(dir, "generated", "custom-ca-configmap.yaml") + + if err := Run(Options{ + OutPath: out, + Namespace: "custom-ns", + CustomCA: true, + CustomCACertificatePEM: testCustomCAPEM, + CustomCAConfigMapOut: configMapOut, + }); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(configMapOut) + if err != nil { + t.Fatal(err) + } + + var configMap corev1.ConfigMap + if err := yaml.Unmarshal(data, &configMap); err != nil { + t.Fatal(err) + } + if configMap.Name != customCAConfigMapName || configMap.Namespace != "custom-ns" { + t.Fatalf("unexpected ConfigMap metadata: %s/%s", configMap.Namespace, configMap.Name) + } + if configMap.Data[customCAConfigMapKey] != testCustomCAPEM { + t.Fatalf("ConfigMap cert data not populated") + } +} + +func assertSelector(t *testing.T, selector corev1.SecretKeySelector, name, key string) { + t.Helper() + if selector.Name != name || selector.Key != key { + t.Fatalf("selector = %s/%s, want %s/%s", selector.Name, selector.Key, name, key) + } +} + +func assertEmptySelector(t *testing.T, selector corev1.SecretKeySelector, field string) { + t.Helper() + if selector.Name != "" || selector.Key != "" { + t.Fatalf("%s selector should be empty, got %s/%s", field, selector.Name, selector.Key) + } +} diff --git a/internal/controller/application_controller.go b/internal/controller/application_controller.go new file mode 100644 index 00000000..eaf2a87c --- /dev/null +++ b/internal/controller/application_controller.go @@ -0,0 +1,1273 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + "fmt" + "reflect" + + gkeGatewayApiNetworkingv1 "github.com/GoogleCloudPlatform/gke-gateway-api/apis/networking/v1" + wandbv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + "github.com/wandb/operator/pkg/utils" + v1alpha1 "github.com/wandb/operator/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "knative.dev/pkg/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +const applicationFinalizer = "applications.apps.wandb.com/finalizer" + +// ApplicationReconciler reconciles a Application object +type ApplicationReconciler struct { + client.Client + Scheme *runtime.Scheme + EnableRollouts bool +} + +// +kubebuilder:rbac:groups=apps.wandb.com,resources=applications,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=apps.wandb.com,resources=applications/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=apps.wandb.com,resources=applications/finalizers,verbs=update +// +kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes/status,verbs=get +// +kubebuilder:rbac:groups=networking.gke.io,resources=healthcheckpolicies,verbs=update;delete;get;list;create;patch;watch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the Application object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/reconcile +func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + ctx, logger := logx.WithSlog(ctx, logx.ReconcileAppV2) + + var app wandbv2.Application + if err := r.Get(ctx, req.NamespacedName, &app); err != nil { + if client.IgnoreNotFound(err) != nil { + logger.Error("unable to fetch Application", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + logger.Info("Application not found. Ignoring since object must be deleted.") + return ctrl.Result{}, nil + } + + logger.Info("Handling Application", "Application", app.Name) + statusBefore := app.DeepCopy().Status + + // Add finalizer if it doesn't exist + if app.DeletionTimestamp == nil { + if !utils.ContainsString(app.GetFinalizers(), applicationFinalizer) { + app.SetFinalizers(append(app.GetFinalizers(), applicationFinalizer)) + if err := r.Update(ctx, &app); err != nil { + logger.Error("Failed to add finalizer", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + logger.Info("Added finalizer to Application") + return ctrl.Result{}, nil + } + } + + if app.DeletionTimestamp != nil { + logger.Info("Application is being deleted") + + // Check if finalizer is present + if utils.ContainsString(app.GetFinalizers(), applicationFinalizer) { + // Perform cleanup based on application kind + switch app.Spec.Kind { + case "Deployment": + if err := r.deleteDeployment(ctx, &app); err != nil { + logger.Error("Failed to delete Deployment during finalization", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + case "StatefulSet": + if err := r.deleteStatefulSet(ctx, &app); err != nil { + logger.Error("Failed to delete StatefulSet during finalization", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + case "Rollout": + if err := r.deleteRollout(ctx, &app); err != nil { + logger.Error("Failed to delete Rollout during finalization", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + } + + // Delete Service if present + if err := r.deleteService(ctx, &app); err != nil { + logger.Error("Failed to delete Service during finalization", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + + // Delete HPA if present + if err := r.deleteHPA(ctx, &app); err != nil { + logger.Error("Failed to delete HPA during finalization", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + + // Delete Jobs + if err := r.deleteJobs(ctx, &app); err != nil { + logger.Error("Failed to delete Jobs during finalization", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + + // Delete CronJobs + if err := r.deleteCronJobs(ctx, &app); err != nil { + logger.Error("Failed to delete CronJobs during finalization", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + + if err := r.deleteHTTPRoute(ctx, &app); err != nil { + logger.Error("Failed to delete HTTPRoute during finalization", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + + // Remove finalizer + app.SetFinalizers(utils.RemoveString(app.GetFinalizers(), applicationFinalizer)) + if err := r.Update(ctx, &app); err != nil { + logger.Error("Failed to remove finalizer", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + logger.Info("Removed finalizer from Application") + } + + return ctrl.Result{}, nil + } + + var result ctrl.Result + var err error + + switch app.Spec.Kind { + case "Deployment": + result, err = r.reconcileDeployment(ctx, &app) + case "Rollout": + result, err = r.reconcileRollout(ctx, &app) + case "StatefulSet": + result, err = r.reconcileStatefulSet(ctx, &app) + case "DaemonSet": + result, err = r.reconcileDaemonSet(ctx, &app) + case "Job": + case "CronJob": + break + default: + logger.Info("Unsupported application kind", "Kind", app.Spec.Kind) + } + + if err != nil { + return result, err + } + + // Always reconcile Jobs regardless of the main application type + if err := r.reconcileJobs(ctx, &app); err != nil { + logger.Error("Failed to reconcile Jobs", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + + // Always reconcile CronJobs regardless of the main application type + if err := r.reconcileCronJobs(ctx, &app); err != nil { + logger.Error("Failed to reconcile CronJobs", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + + // Reconcile Service if specified + if err := r.reconcileService(ctx, &app); err != nil { + logger.Error("Failed to reconcile Service", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + + // Reconcile HPA if specified + if err := r.reconcileHPA(ctx, &app); err != nil { + logger.Error("Failed to reconcile HPA", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + + if err := r.reconcileHTTPRoute(ctx, &app); err != nil { + logger.Error("Failed to reconcile HTTPRoute", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + + app.Status.Ready = false + if app.Status.DeploymentStatus != nil { + if app.Status.DeploymentStatus.ReadyReplicas == app.Status.DeploymentStatus.Replicas && + app.Status.DeploymentStatus.Replicas > 0 { + app.Status.Ready = true + } + } else if app.Status.StatefulSetStatus != nil { + if app.Status.StatefulSetStatus.ReadyReplicas == app.Status.StatefulSetStatus.Replicas && + app.Status.StatefulSetStatus.Replicas > 0 { + app.Status.Ready = true + } + } else if app.Status.RolloutStatus != nil { + if app.Status.RolloutStatus.ReadyReplicas == app.Status.RolloutStatus.Replicas && + app.Status.RolloutStatus.Replicas > 0 { + app.Status.Ready = true + } + } + + if apiequality.Semantic.DeepEqual(statusBefore, app.Status) { + return result, nil + } + if err := r.Status().Update(ctx, &app); err != nil { + logger.Error("Failed to update Application status", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + + return result, nil +} + +// reconcileDeployment handles Deployment type applications +func (r *ApplicationReconciler) reconcileDeployment(ctx context.Context, app *wandbv2.Application) (ctrl.Result, error) { + logger := logx.GetSlog(ctx) + logger.Info("Reconciling Deployment", "Application", app.Name) + + deployment := &appsv1.Deployment{} + err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, deployment) + before := deployment.DeepCopy() + if err != nil { + if client.IgnoreNotFound(err) != nil { + logger.Error("Failed to get Deployment", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + logger.Info("Deployment not found", "Deployment", app.Name) + } + + selectorLabels := getSelectorLabels(app) + + deployment.Name = app.Name + deployment.Namespace = app.Namespace + + deployment.Spec.Template.Spec = r.defaultPodSpec(app.Spec.PodTemplate.Spec) + deployment.Spec.Template.SetLabels( + utils.MergeMapsStringString( + deployment.Spec.Template.GetLabels(), + app.Spec.MetaTemplate.Labels, + app.Spec.PodTemplate.GetLabels(), + selectorLabels, + )) + + deployment.Spec.Template.SetAnnotations( + utils.MergeMapsStringString( + deployment.Spec.Template.GetAnnotations(), + app.Spec.MetaTemplate.Annotations, + app.Spec.PodTemplate.GetAnnotations(), + )) + + deployment.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: selectorLabels, + } + + if app.Spec.HpaTemplate != nil { + if deployment.CreationTimestamp.IsZero() { + deployment.Spec.Replicas = app.Spec.HpaTemplate.MinReplicas + } + // Do not update replicas if HPA is managing them + } else { + deployment.Spec.Replicas = app.Spec.Replicas + } + + if err = controllerutil.SetControllerReference(app, deployment, r.Scheme); err != nil { + return ctrl.Result{}, err + } + + logger.Debug( + "Desired Deployment", + "Deployment", deployment.Name, + "containers", len(deployment.Spec.Template.Spec.Containers), + "initContainers", len(deployment.Spec.Template.Spec.InitContainers), + ) + + if deployment.CreationTimestamp.IsZero() { + if err := r.Create(ctx, deployment); err != nil { + logger.Error("Failed to create Deployment", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + } else if !deploymentManagedFieldsEqual(before, deployment) { + if err := r.Update(ctx, deployment); err != nil { + logger.Error("Failed to update Deployment", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + } + + app.Status.DeploymentStatus = &deployment.Status + + logger.Info("Successfully reconciled Deployment", "Deployment", deployment.Name) + return ctrl.Result{}, nil +} + +func getSelectorLabels(app *wandbv2.Application) map[string]string { + return map[string]string{ + "app.kubernetes.io/name": app.Name, + "app.kubernetes.io/instance": app.Namespace, + } +} + +func managedObjectMetadataEqual(before, after client.Object) bool { + return apiequality.Semantic.DeepEqual(before.GetLabels(), after.GetLabels()) && + apiequality.Semantic.DeepEqual(before.GetAnnotations(), after.GetAnnotations()) && + apiequality.Semantic.DeepEqual(before.GetOwnerReferences(), after.GetOwnerReferences()) +} + +func (r *ApplicationReconciler) defaultPodSpec(spec corev1.PodSpec) corev1.PodSpec { + pod := &corev1.Pod{Spec: *spec.DeepCopy()} + if r.Scheme != nil { + r.Scheme.Default(pod) + } + return pod.Spec +} + +func deploymentManagedFieldsEqual(before, after *appsv1.Deployment) bool { + return managedObjectMetadataEqual(before, after) && + apiequality.Semantic.DeepEqual(before.Spec, after.Spec) +} + +func rolloutManagedFieldsEqual(before, after *v1alpha1.Rollout) bool { + return managedObjectMetadataEqual(before, after) && reflect.DeepEqual(before.Spec, after.Spec) +} + +func statefulSetManagedFieldsEqual(before, after *appsv1.StatefulSet) bool { + return managedObjectMetadataEqual(before, after) && + apiequality.Semantic.DeepEqual(before.Spec, after.Spec) +} + +// deleteDeployment deletes the Deployment associated with the Application +func (r *ApplicationReconciler) deleteDeployment(ctx context.Context, app *wandbv2.Application) error { + logger := logx.GetSlog(ctx) + logger.Info("Deleting Deployment", "Application", app.Name) + + deployment := &appsv1.Deployment{} + err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, deployment) + if err != nil { + if errors.IsNotFound(err) { + logger.Info("Deployment not found, nothing to delete", "Deployment", app.Name) + return nil + } + logger.Error("Failed to get Deployment", logx.ErrAttr(err)) + return err + } + + deletePolicy := client.PropagationPolicy(metav1.DeletePropagationBackground) + if err := r.Delete(ctx, deployment, deletePolicy); err != nil { + logger.Error("Failed to delete Deployment", logx.ErrAttr(err), "Deployment", app.Name) + return err + } + logger.Info("Successfully deleted Deployment", "Deployment", app.Name) + + return nil +} + +// reconcileRollout handles Rollout type applications +func (r *ApplicationReconciler) reconcileRollout(ctx context.Context, app *wandbv2.Application) (ctrl.Result, error) { + logger := logx.GetSlog(ctx) + logger.Info("Reconciling Rollout", "Application", app.Name) + + rollout := &v1alpha1.Rollout{} + err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, rollout) + before := rollout.DeepCopy() + if err != nil { + if client.IgnoreNotFound(err) != nil { + logger.Error("Failed to get Rollout", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + logger.Info("Rollout not found", "Rollout", app.Name) + } + + selectorLabels := getSelectorLabels(app) + + rollout.Name = app.Name + rollout.Namespace = app.Namespace + + rollout.Spec.Template.Spec = r.defaultPodSpec(app.Spec.PodTemplate.Spec) + rollout.Spec.Template.SetLabels( + utils.MergeMapsStringString( + rollout.Spec.Template.GetLabels(), + app.Spec.MetaTemplate.Labels, + app.Spec.PodTemplate.GetLabels(), + selectorLabels, + )) + + rollout.Spec.Template.SetAnnotations( + utils.MergeMapsStringString( + rollout.Spec.Template.GetAnnotations(), + app.Spec.MetaTemplate.Annotations, + app.Spec.PodTemplate.GetAnnotations(), + )) + + rollout.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: selectorLabels, + } + + if app.Spec.HpaTemplate != nil { + if rollout.CreationTimestamp.IsZero() { + rollout.Spec.Replicas = app.Spec.HpaTemplate.MinReplicas + } + // Do not update replicas if HPA is managing them + } else { + rollout.Spec.Replicas = app.Spec.Replicas + } + + if err = controllerutil.SetControllerReference(app, rollout, r.Scheme); err != nil { + return ctrl.Result{}, err + } + + logger.Debug( + "Desired Rollout", + "Rollout", rollout.Name, + "containers", len(rollout.Spec.Template.Spec.Containers), + "initContainers", len(rollout.Spec.Template.Spec.InitContainers), + ) + + if rollout.CreationTimestamp.IsZero() { + if err := r.Create(ctx, rollout); err != nil { + logger.Error("Failed to create Rollout", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + } else if !rolloutManagedFieldsEqual(before, rollout) { + if err := r.Update(ctx, rollout); err != nil { + logger.Error("Failed to update Rollout", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + } + + app.Status.RolloutStatus = &rollout.Status + + logger.Info("Successfully reconciled Rollout", "Rollout", rollout.Name) + return ctrl.Result{}, nil +} + +// deleteRollout deletes the Rollout associated with the Application +func (r *ApplicationReconciler) deleteRollout(ctx context.Context, app *wandbv2.Application) error { + logger := logx.GetSlog(ctx) + logger.Info("Deleting Rollout", "Application", app.Name) + + rollout := &v1alpha1.Rollout{} + err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, rollout) + if err != nil { + if errors.IsNotFound(err) { + logger.Info("Rollout not found, nothing to delete", "Rollout", app.Name) + return nil + } + logger.Error("Failed to get Rollout", logx.ErrAttr(err)) + return err + } + + deletePolicy := client.PropagationPolicy(metav1.DeletePropagationBackground) + if err := r.Delete(ctx, rollout, deletePolicy); err != nil { + logger.Error("Failed to delete Rollout", logx.ErrAttr(err), "Rollout", app.Name) + return err + } + logger.Info("Successfully deleted Rollout", "Rollout", app.Name) + + return nil +} + +// reconcileStatefulSet handles StatefulSet type applications +func (r *ApplicationReconciler) reconcileStatefulSet(ctx context.Context, app *wandbv2.Application) (ctrl.Result, error) { + logger := logx.GetSlog(ctx) + logger.Info("Reconciling StatefulSet", "Application", app.Name) + + statefulSet := &appsv1.StatefulSet{} + err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, statefulSet) + before := statefulSet.DeepCopy() + if err != nil { + if client.IgnoreNotFound(err) != nil { + logger.Error("Failed to get StatefulSet", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + logger.Info("StatefulSet not found", "StatefulSet", app.Name) + } + + selectorLabels := getSelectorLabels(app) + + statefulSet.Name = app.Name + statefulSet.Namespace = app.Namespace + + statefulSet.Spec.Template.Spec = r.defaultPodSpec(app.Spec.PodTemplate.Spec) + statefulSet.Spec.Template.SetLabels( + utils.MergeMapsStringString( + statefulSet.Spec.Template.GetLabels(), + app.Spec.MetaTemplate.Labels, + app.Spec.PodTemplate.GetLabels(), + selectorLabels, + )) + + statefulSet.Spec.Template.SetAnnotations( + utils.MergeMapsStringString( + statefulSet.Spec.Template.GetAnnotations(), + app.Spec.MetaTemplate.Annotations, + app.Spec.PodTemplate.GetAnnotations(), + )) + + statefulSet.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: selectorLabels, + } + + // ServiceName governs the StatefulSet's stable pod DNS. It is immutable once + // set, so only apply it at creation time. + if statefulSet.CreationTimestamp.IsZero() && app.Spec.ServiceName != "" { + statefulSet.Spec.ServiceName = app.Spec.ServiceName + } + + // VolumeClaimTemplates are immutable once a StatefulSet exists, so only set + // them at creation time to avoid update errors from the apiserver. + if statefulSet.CreationTimestamp.IsZero() && len(app.Spec.VolumeClaimTemplates) > 0 { + templates := make([]corev1.PersistentVolumeClaim, len(app.Spec.VolumeClaimTemplates)) + for i := range app.Spec.VolumeClaimTemplates { + templates[i] = *app.Spec.VolumeClaimTemplates[i].DeepCopy() + } + statefulSet.Spec.VolumeClaimTemplates = templates + } + + if app.Spec.HpaTemplate != nil { + if statefulSet.CreationTimestamp.IsZero() { + statefulSet.Spec.Replicas = app.Spec.HpaTemplate.MinReplicas + } + // Do not update replicas if HPA is managing them + } else { + statefulSet.Spec.Replicas = app.Spec.Replicas + } + + if err = controllerutil.SetControllerReference(app, statefulSet, r.Scheme); err != nil { + return ctrl.Result{}, err + } + + logger.Debug( + "Desired StatefulSet", + "StatefulSet", statefulSet.Name, + "containers", len(statefulSet.Spec.Template.Spec.Containers), + "initContainers", len(statefulSet.Spec.Template.Spec.InitContainers), + ) + + if statefulSet.CreationTimestamp.IsZero() { + if err := r.Create(ctx, statefulSet); err != nil { + logger.Error("Failed to create StatefulSet", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + } else if !statefulSetManagedFieldsEqual(before, statefulSet) { + if err := r.Update(ctx, statefulSet); err != nil { + logger.Error("Failed to update StatefulSet", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + } + + app.Status.StatefulSetStatus = &statefulSet.Status + + logger.Info("Successfully reconciled StatefulSet", "StatefulSet", statefulSet.Name) + return ctrl.Result{}, nil +} + +// deleteStatefulSet deletes the StatefulSet associated with the Application +func (r *ApplicationReconciler) deleteStatefulSet(ctx context.Context, app *wandbv2.Application) error { + logger := logx.GetSlog(ctx) + logger.Info("Deleting StatefulSet", "Application", app.Name) + + statefulSet := &appsv1.StatefulSet{} + err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, statefulSet) + if err != nil { + if errors.IsNotFound(err) { + logger.Info("StatefulSet not found, nothing to delete", "StatefulSet", app.Name) + return nil + } + logger.Error("Failed to get StatefulSet", logx.ErrAttr(err)) + return err + } + + deletePolicy := client.PropagationPolicy(metav1.DeletePropagationBackground) + if err := r.Delete(ctx, statefulSet, deletePolicy); err != nil { + logger.Error("Failed to delete StatefulSet", logx.ErrAttr(err), "StatefulSet", app.Name) + return err + } + logger.Info("Successfully deleted StatefulSet", "StatefulSet", app.Name) + + return nil +} + +// reconcileDaemonSet handles DaemonSet type applications +func (r *ApplicationReconciler) reconcileDaemonSet(ctx context.Context, app *wandbv2.Application) (ctrl.Result, error) { + logger := logx.GetSlog(ctx) + logger.Info("Reconciling DaemonSet", "Application", app.Name) + return ctrl.Result{}, nil +} + +// reconcileJobs handles multiple Job resources defined in the Application spec +func (r *ApplicationReconciler) reconcileJobs(ctx context.Context, app *wandbv2.Application) error { + logger := logx.GetSlog(ctx) + + for i, job := range app.Spec.Jobs { + jobName := job.Name + if jobName == "" { + jobName = fmt.Sprintf("%s-job-%d", app.Name, i) + } + + logger.Info("Reconciling Job", "Application", app.Name, "Job", jobName) + + currentJob := &batchv1.Job{} + err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: jobName}, currentJob) + + // Create or update the job + if err != nil && !errors.IsNotFound(err) { + logger.Error("Failed to get Job", logx.ErrAttr(err), "Job", jobName) + return err + } + + // Set up the job with the provided spec + jobToReconcile := job.DeepCopy() + jobToReconcile.Name = jobName + jobToReconcile.Namespace = app.Namespace + + // Ensure the job has proper labels + if jobToReconcile.Labels == nil { + jobToReconcile.Labels = make(map[string]string) + } + jobToReconcile.Labels["app.kubernetes.io/name"] = app.Name + jobToReconcile.Labels["app.kubernetes.io/instance"] = app.Namespace + jobToReconcile.Labels["app.kubernetes.io/managed-by"] = "application-controller" + + if err = controllerutil.SetControllerReference(app, jobToReconcile, r.Scheme); err != nil { + return err + } + + if currentJob.CreationTimestamp.IsZero() { + if err := r.Create(ctx, jobToReconcile); err != nil { + logger.Error("Failed to create Job", logx.ErrAttr(err), "Job", jobName) + return err + } + logger.Info("Successfully created Job", "Job", jobName) + } else { + // Jobs cannot be updated, so we need to check if the spec has changed + // and delete + recreate if necessary + if !reflect.DeepEqual(currentJob.Spec, jobToReconcile.Spec) { + logger.Info("Job spec has changed, deleting and recreating", "Job", jobName) + + // Delete the existing job + deletePolicy := client.PropagationPolicy(metav1.DeletePropagationBackground) + if err := r.Delete(ctx, currentJob, deletePolicy); err != nil { + logger.Error("Failed to delete Job", logx.ErrAttr(err), "Job", jobName) + return err + } + logger.Info("Successfully deleted Job", "Job", jobName) + + // Recreate the job with new spec + if err := r.Create(ctx, jobToReconcile); err != nil { + logger.Error("Failed to recreate Job", logx.ErrAttr(err), "Job", jobName) + return err + } + logger.Info("Successfully recreated Job", "Job", jobName) + } else { + logger.Info("Job spec unchanged, no action needed", "Job", jobName) + } + } + } + + return nil +} + +// deleteJobs deletes all Jobs associated with the Application +func (r *ApplicationReconciler) deleteJobs(ctx context.Context, app *wandbv2.Application) error { + logger := logx.GetSlog(ctx) + logger.Info("Deleting Jobs", "Application", app.Name) + + jobList := &batchv1.JobList{} + listOpts := []client.ListOption{ + client.InNamespace(app.Namespace), + client.MatchingLabels{ + "app.kubernetes.io/name": app.Name, + "app.kubernetes.io/instance": app.Namespace, + "app.kubernetes.io/managed-by": "application-controller", + }, + } + + if err := r.List(ctx, jobList, listOpts...); err != nil { + logger.Error("Failed to list Jobs", logx.ErrAttr(err)) + return err + } + + deletePolicy := client.PropagationPolicy(metav1.DeletePropagationBackground) + for _, job := range jobList.Items { + logger.Info("Deleting Job", "Job", job.Name) + if err := r.Delete(ctx, &job, deletePolicy); err != nil { + logger.Error("Failed to delete Job", logx.ErrAttr(err), "Job", job.Name) + return err + } + logger.Info("Successfully deleted Job", "Job", job.Name) + } + + return nil +} + +// reconcileCronJobs handles multiple CronJob resources defined in the Application spec +func (r *ApplicationReconciler) reconcileCronJobs(ctx context.Context, app *wandbv2.Application) error { + logger := logx.GetSlog(ctx) + + for i, cronJob := range app.Spec.CronJobs { + cronJobName := cronJob.Name + if cronJobName == "" { + cronJobName = fmt.Sprintf("%s-cronjob-%d", app.Name, i) + } + + logger.Info("Reconciling CronJob", "Application", app.Name, "CronJob", cronJobName) + + currentCronJob := &batchv1.CronJob{} + err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: cronJobName}, currentCronJob) + + // Check if we got an error that's not a NotFound + if err != nil && client.IgnoreNotFound(err) != nil { + logger.Error("Failed to get CronJob", logx.ErrAttr(err), "CronJob", cronJobName) + return err + } + + // Set up the cronjob with the provided spec + cronJobToReconcile := cronJob.DeepCopy() + cronJobToReconcile.Name = cronJobName + cronJobToReconcile.Namespace = app.Namespace + + // Ensure the cronjob has proper labels + if cronJobToReconcile.Labels == nil { + cronJobToReconcile.Labels = make(map[string]string) + } + cronJobToReconcile.Labels["app.kubernetes.io/name"] = app.Name + cronJobToReconcile.Labels["app.kubernetes.io/instance"] = app.Namespace + cronJobToReconcile.Labels["app.kubernetes.io/managed-by"] = "application-controller" + + if err = controllerutil.SetControllerReference(app, cronJobToReconcile, r.Scheme); err != nil { + return err + } + + if currentCronJob.CreationTimestamp.IsZero() { + if err := r.Create(ctx, cronJobToReconcile); err != nil { + logger.Error("Failed to create CronJob", logx.ErrAttr(err), "CronJob", cronJobName) + return err + } + logger.Info("Successfully created CronJob", "CronJob", cronJobName) + } else { + // Update existing cronjob + cronJobToReconcile.ResourceVersion = currentCronJob.ResourceVersion + if managedObjectMetadataEqual(currentCronJob, cronJobToReconcile) && + apiequality.Semantic.DeepEqual(currentCronJob.Spec, cronJobToReconcile.Spec) { + continue + } + if err := r.Update(ctx, cronJobToReconcile); err != nil { + logger.Error("Failed to update CronJob", logx.ErrAttr(err), "CronJob", cronJobName) + return err + } + logger.Info("Successfully updated CronJob", "CronJob", cronJobName) + } + } + + return nil +} + +// deleteCronJobs deletes all CronJobs associated with the Application +func (r *ApplicationReconciler) deleteCronJobs(ctx context.Context, app *wandbv2.Application) error { + logger := logx.GetSlog(ctx) + logger.Info("Deleting CronJobs", "Application", app.Name) + + cronJobList := &batchv1.CronJobList{} + listOpts := []client.ListOption{ + client.InNamespace(app.Namespace), + client.MatchingLabels{ + "app.kubernetes.io/name": app.Name, + "app.kubernetes.io/instance": app.Namespace, + "app.kubernetes.io/managed-by": "application-controller", + }, + } + + if err := r.List(ctx, cronJobList, listOpts...); err != nil { + logger.Error("Failed to list CronJobs", logx.ErrAttr(err)) + return err + } + + deletePolicy := client.PropagationPolicy(metav1.DeletePropagationBackground) + for _, cronJob := range cronJobList.Items { + logger.Info("Deleting CronJob", "CronJob", cronJob.Name) + if err := r.Delete(ctx, &cronJob, deletePolicy); err != nil { + logger.Error("Failed to delete CronJob", logx.ErrAttr(err), "CronJob", cronJob.Name) + return err + } + logger.Info("Successfully deleted CronJob", "CronJob", cronJob.Name) + } + + return nil +} + +// reconcileService ensures a Service exists/updated when specified in the Application spec +func (r *ApplicationReconciler) reconcileService(ctx context.Context, app *wandbv2.Application) error { + logger := logx.GetSlog(ctx) + + if app.Spec.ServiceTemplate == nil { + // Nothing to reconcile + return nil + } + + desired := &corev1.Service{} + desired.Name = app.Name + desired.Namespace = app.Namespace + + // Merge labels/annotations from meta template + desired.Labels = utils.MergeMapsStringString( + desired.Labels, + app.Spec.MetaTemplate.Labels, + ) + desired.Annotations = utils.MergeMapsStringString( + desired.Annotations, + app.Spec.MetaTemplate.Annotations, + ) + // The API server stores empty annotations as nil; keep them nil so the + // steady-state metadata compare below settles. + if len(desired.Annotations) == 0 { + desired.Annotations = nil + } + + // Copy spec from template, filling the port defaults the API server would + // apply (templates written before normalization may lack them). + desired.Spec = *app.Spec.ServiceTemplate.DeepCopy() + common.NormalizeServicePorts(desired.Spec.Ports) + + // Ensure selector targets the application's pods + selectorLabels := getSelectorLabels(app) + // Merge provided selector with our standard labels + desired.Spec.Selector = utils.MergeMapsStringString(desired.Spec.Selector, selectorLabels) + // Also add selector labels to the Service's metadata labels so they are queryable on the Service itself + desired.Labels = utils.MergeMapsStringString(desired.Labels, selectorLabels) + + if err := controllerutil.SetControllerReference(app, desired, r.Scheme); err != nil { + return err + } + + current := &corev1.Service{} + err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, current) + if err != nil { + if !errors.IsNotFound(err) { + logger.Error("Failed to get Service", logx.ErrAttr(err)) + return err + } + // Create path + logger.Info("Creating Service", "Service", desired.Name) + if err := r.Create(ctx, desired); err != nil { + logger.Error("Failed to create Service", logx.ErrAttr(err)) + return err + } + + app.Status.ServiceStatus = &desired.Status + + logger.Info("Successfully created Service", "Service", desired.Name) + return nil + } + before := current.DeepCopy() + + // Update path: preserve immutable fields + desired.ResourceVersion = current.ResourceVersion + desired.Spec.ClusterIP = current.Spec.ClusterIP + desired.Spec.ClusterIPs = current.Spec.ClusterIPs + desired.Spec.IPFamilies = current.Spec.IPFamilies + desired.Spec.IPFamilyPolicy = current.Spec.IPFamilyPolicy + desired.Spec.HealthCheckNodePort = current.Spec.HealthCheckNodePort + preserveServerDefaultedServiceFields(&desired.Spec, ¤t.Spec) + + // Only update if there are changes + // Apply desired into current to minimize overwrite + current.Labels = desired.Labels + current.Annotations = desired.Annotations + current.Spec.Ports = desired.Spec.Ports + current.Spec.Type = desired.Spec.Type + current.Spec.Selector = desired.Spec.Selector + current.Spec.SessionAffinity = desired.Spec.SessionAffinity + current.Spec.ExternalTrafficPolicy = desired.Spec.ExternalTrafficPolicy + current.Spec.InternalTrafficPolicy = desired.Spec.InternalTrafficPolicy + current.Spec.LoadBalancerClass = desired.Spec.LoadBalancerClass + current.Spec.AllocateLoadBalancerNodePorts = desired.Spec.AllocateLoadBalancerNodePorts + current.Spec.ExternalIPs = desired.Spec.ExternalIPs + current.Spec.LoadBalancerIP = desired.Spec.LoadBalancerIP + current.Spec.LoadBalancerSourceRanges = desired.Spec.LoadBalancerSourceRanges + + if !managedObjectMetadataEqual(before, current) || !apiequality.Semantic.DeepEqual(before.Spec, current.Spec) { + logger.Info("Updating Service", "Service", current.Name) + if err := r.Update(ctx, current); err != nil { + logger.Error("Failed to update Service", logx.ErrAttr(err)) + return err + } + logger.Info("Successfully updated Service", "Service", current.Name) + } + + app.Status.ServiceStatus = ¤t.Status + + return nil +} + +// preserveServerDefaultedServiceFields keeps API-server-defaulted or -allocated +// values for fields the template leaves unset, so a settled Service compares +// equal and Update only fires on real changes. +func preserveServerDefaultedServiceFields(desired, current *corev1.ServiceSpec) { + if desired.Type == "" { + desired.Type = current.Type + } + if desired.SessionAffinity == "" { + desired.SessionAffinity = current.SessionAffinity + } + if desired.ExternalTrafficPolicy == "" { + desired.ExternalTrafficPolicy = current.ExternalTrafficPolicy + } + if desired.InternalTrafficPolicy == nil { + desired.InternalTrafficPolicy = current.InternalTrafficPolicy + } + if desired.AllocateLoadBalancerNodePorts == nil { + desired.AllocateLoadBalancerNodePorts = current.AllocateLoadBalancerNodePorts + } + if desired.Type != corev1.ServiceTypeNodePort && desired.Type != corev1.ServiceTypeLoadBalancer { + return + } + // NodePorts are allocated by the API server; keep them unless the template pins one. + for i := range desired.Ports { + p := &desired.Ports[i] + if p.NodePort != 0 { + continue + } + for j := range current.Ports { + if current.Ports[j].Name == p.Name { + p.NodePort = current.Ports[j].NodePort + break + } + } + } +} + +// deleteService deletes the Service associated with the Application +func (r *ApplicationReconciler) deleteService(ctx context.Context, app *wandbv2.Application) error { + logger := logx.GetSlog(ctx) + svc := &corev1.Service{} + if err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, svc); err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + deletePolicy := client.PropagationPolicy(metav1.DeletePropagationBackground) + if err := r.Delete(ctx, svc, deletePolicy); err != nil { + logger.Error("Failed to delete Service", logx.ErrAttr(err), "Service", app.Name) + return err + } + logger.Info("Successfully deleted Service", "Service", app.Name) + return nil +} + +// reconcileHPA handles HorizontalPodAutoscaler resources defined in the Application spec +func (r *ApplicationReconciler) reconcileHPA(ctx context.Context, app *wandbv2.Application) error { + logger := logx.GetSlog(ctx) + + if app.Spec.HpaTemplate == nil { + // If HPA template is not specified, ensure any existing HPA owned by the Application is deleted + return r.deleteHPA(ctx, app) + } + + desired := &autoscalingv2.HorizontalPodAutoscaler{} + desired.Name = app.Name + desired.Namespace = app.Namespace + + // Set owner reference + if err := ctrl.SetControllerReference(app, desired, r.Scheme); err != nil { + return err + } + + // Merge labels/annotations from meta template + desired.Labels = utils.MergeMapsStringString( + desired.Labels, + app.Spec.MetaTemplate.Labels, + ) + desired.Annotations = utils.MergeMapsStringString( + desired.Annotations, + app.Spec.MetaTemplate.Annotations, + ) + + // Copy spec from template + desired.Spec = *app.Spec.HpaTemplate.DeepCopy() + + // Ensure scaleTargetRef points to our application workload + var groupVersion string + var kind string + switch app.Spec.Kind { + case "Deployment": + groupVersion = appsv1.SchemeGroupVersion.String() + kind = "Deployment" + case "StatefulSet": + groupVersion = appsv1.SchemeGroupVersion.String() + kind = "StatefulSet" + case "Rollout": + groupVersion = v1alpha1.SchemeGroupVersion.String() + kind = "Rollout" + default: + return fmt.Errorf("unsupported application kind for HPA: %s", app.Spec.Kind) + } + + desired.Spec.ScaleTargetRef = autoscalingv2.CrossVersionObjectReference{ + APIVersion: groupVersion, + Kind: kind, + Name: app.Name, + } + + if err := controllerutil.SetControllerReference(app, desired, r.Scheme); err != nil { + return err + } + + current := &autoscalingv2.HorizontalPodAutoscaler{} + err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, current) + if err != nil { + if !errors.IsNotFound(err) { + logger.Error("Failed to get HPA", logx.ErrAttr(err)) + return err + } + // Create path + logger.Info("Creating HPA", "HPA", desired.Name) + if err := r.Create(ctx, desired); err != nil { + logger.Error("Failed to create HPA", logx.ErrAttr(err)) + return err + } + + app.Status.HPAStatus = &desired.Status + logger.Info("Successfully created HPA", "HPA", desired.Name) + return nil + } + + // Update path + desired.ResourceVersion = current.ResourceVersion + if managedObjectMetadataEqual(current, desired) && apiequality.Semantic.DeepEqual(current.Spec, desired.Spec) { + app.Status.HPAStatus = ¤t.Status + return nil + } + logger.Info("Updating HPA", "HPA", desired.Name) + if err := r.Update(ctx, desired); err != nil { + logger.Error("Failed to update HPA", logx.ErrAttr(err)) + return err + } + + app.Status.HPAStatus = ¤t.Status + logger.Info("Successfully updated HPA", "HPA", desired.Name) + return nil +} + +// deleteHPA deletes the HPA associated with the Application +func (r *ApplicationReconciler) deleteHPA(ctx context.Context, app *wandbv2.Application) error { + logger := logx.GetSlog(ctx) + hpa := &autoscalingv2.HorizontalPodAutoscaler{} + if err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, hpa); err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + deletePolicy := client.PropagationPolicy(metav1.DeletePropagationBackground) + if err := r.Delete(ctx, hpa, deletePolicy); err != nil { + logger.Error("Failed to delete HPA", logx.ErrAttr(err), "HPA", app.Name) + return err + } + logger.Info("Successfully deleted HPA", "HPA", app.Name) + return nil +} + +func (r *ApplicationReconciler) reconcileHTTPRoute(ctx context.Context, app *wandbv2.Application) error { + if app.Spec.HTTPRouteTemplate == nil { + if err := r.deleteHTTPRoute(ctx, app); err != nil { + return err + } + app.Status.HTTPRouteStatus = nil + return nil + } + + logger := logx.GetSlog(ctx) + + httpRoute := &gatewayv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: app.Name, + Namespace: app.Namespace, + }, + } + op, err := ctrl.CreateOrUpdate(ctx, r.Client, httpRoute, func() error { + httpRoute.Labels = utils.MergeMapsStringString(httpRoute.Labels, app.Spec.MetaTemplate.Labels) + httpRoute.Annotations = utils.MergeMapsStringString(httpRoute.Annotations, app.Spec.MetaTemplate.Annotations) + httpRoute.Spec.ParentRefs = app.Spec.HTTPRouteTemplate.ParentRefs + httpRoute.Spec.Hostnames = app.Spec.HTTPRouteTemplate.Hostnames + httpRoute.Spec.Rules = buildHTTPRouteRules(app) + if err := ctrl.SetControllerReference(app, httpRoute, r.Scheme); err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + app.Status.HTTPRouteStatus = summarizeHTTPRouteStatus(httpRoute) + logger.Info(fmt.Sprintf("Successfully %s HTTPRoute", op), "HTTPRoute", httpRoute.Name) + + if len(httpRoute.Status.Parents) > 0 && httpRoute.Status.Parents[0].ControllerName == "networking.gke.io/gateway" { + healthCheckPolicy := &gkeGatewayApiNetworkingv1.HealthCheckPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: app.Name, + Namespace: app.Namespace, + }, + } + op, err = ctrl.CreateOrUpdate(ctx, r.Client, healthCheckPolicy, func() error { + healthCheckPolicy.Labels = utils.MergeMapsStringString(healthCheckPolicy.Labels, app.Spec.MetaTemplate.Labels) + healthCheckPolicy.Annotations = utils.MergeMapsStringString(healthCheckPolicy.Annotations, app.Spec.MetaTemplate.Annotations) + healthCheckPolicy.Spec.Default = &gkeGatewayApiNetworkingv1.HealthCheckPolicyConfig{ + CheckIntervalSec: ptr.Int64(5), + TimeoutSec: ptr.Int64(5), + UnhealthyThreshold: ptr.Int64(3), + HealthyThreshold: ptr.Int64(2), + Config: &gkeGatewayApiNetworkingv1.HealthCheck{ + Type: gkeGatewayApiNetworkingv1.HTTP, + HTTP: &gkeGatewayApiNetworkingv1.HTTPHealthCheck{ + CommonHealthCheck: gkeGatewayApiNetworkingv1.CommonHealthCheck{}, + CommonHTTPHealthCheck: gkeGatewayApiNetworkingv1.CommonHTTPHealthCheck{ + RequestPath: ptr.String(app.Spec.PodTemplate.Spec.Containers[0].ReadinessProbe.HTTPGet.Path), + }, + }, + }, + } + healthCheckPolicy.Spec.TargetRef = gatewayv1alpha2.NamespacedPolicyTargetReference{ + Kind: "Service", + Name: gatewayv1alpha2.ObjectName(app.Name), + } + if err != nil { + return err + } + + if err := ctrl.SetControllerReference(app, healthCheckPolicy, r.Scheme); err != nil { + return err + } + return nil + }) + if err != nil { + logger.Error("Failed to create or update health check policy", "HealthCheckPolicy", healthCheckPolicy.Name, logx.ErrAttr(err)) + return err + } + logger.Info(fmt.Sprintf("Successfully %s HealthCheckPolicy", op), "HealthCheckPolicy", healthCheckPolicy.Name) + } + + return nil +} + +func buildHTTPRouteRules(app *wandbv2.Application) []gatewayv1.HTTPRouteRule { + tmpl := app.Spec.HTTPRouteTemplate + + paths := tmpl.Paths + if len(paths) == 0 { + paths = []string{"/"} + } + + var matches []gatewayv1.HTTPRouteMatch + for _, p := range paths { + p := p + matchType := gatewayv1.PathMatchPathPrefix + if tmpl.PathType == "Exact" { + matchType = gatewayv1.PathMatchExact + } + matches = append(matches, gatewayv1.HTTPRouteMatch{ + Path: &gatewayv1.HTTPPathMatch{ + Type: &matchType, + Value: &p, + }, + }) + } + + backendRef := gatewayv1.HTTPBackendRef{ + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Name: gatewayv1.ObjectName(app.Name), + Port: tmpl.ServicePort, + }, + }, + } + + return []gatewayv1.HTTPRouteRule{{ + Matches: matches, + BackendRefs: []gatewayv1.HTTPBackendRef{backendRef}, + Filters: []gatewayv1.HTTPRouteFilter{ + { + Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, + RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{ + Remove: []string{"X-Forwarded-Host", "X-Forwarded-Port"}, + }, + }, + }, + }} +} + +func (r *ApplicationReconciler) deleteHTTPRoute(ctx context.Context, app *wandbv2.Application) error { + route := &gatewayv1.HTTPRoute{} + if !utils.IsRegistered(r.Scheme, route) { + return nil + } + if err := r.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: app.Name}, route); err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + return r.Delete(ctx, route, client.PropagationPolicy(metav1.DeletePropagationBackground)) +} + +func summarizeHTTPRouteStatus(route *gatewayv1.HTTPRoute) *wandbv2.HTTPRouteStatusSummary { + if route == nil { + return nil + } + + summary := &wandbv2.HTTPRouteStatusSummary{} + for _, parent := range route.Status.Parents { + if apimeta.IsStatusConditionTrue(parent.Conditions, string(gatewayv1.RouteConditionAccepted)) { + summary.Accepted = true + break + } + } + + return summary +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error { + controller := ctrl.NewControllerManagedBy(mgr). + For(&wandbv2.Application{}). + Owns(&appsv1.Deployment{}). + Owns(&appsv1.StatefulSet{}). + Owns(&corev1.Service{}). + Owns(&autoscalingv2.HorizontalPodAutoscaler{}). + Named("application") + + if utils.IsRegistered(r.Scheme, &v1alpha1.Rollout{}) { + controller = controller.Owns(&v1alpha1.Rollout{}) + } + + if utils.IsRegistered(r.Scheme, &gatewayv1.Gateway{}) { + controller = controller.Owns(&gatewayv1.HTTPRoute{}) + } + + return controller.Complete(r) +} diff --git a/internal/controller/application_controller_test.go b/internal/controller/application_controller_test.go new file mode 100644 index 00000000..dee29a7f --- /dev/null +++ b/internal/controller/application_controller_test.go @@ -0,0 +1,743 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// serviceUpdateCounter counts Service Update calls passing through the client. +type serviceUpdateCounter struct { + client.Client + updates int +} + +func (c *serviceUpdateCounter) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + if _, ok := obj.(*corev1.Service); ok { + c.updates++ + } + return c.Client.Update(ctx, obj, opts...) +} + +var _ = Describe("Application Controller", func() { + Context("When reconciling a resource", func() { + ctx := context.Background() + + AfterEach(func() { + // Cleanup all Applications in the namespace + appList := &apiv2.ApplicationList{} + Expect(k8sClient.List(ctx, appList, client.InNamespace("default"))).To(Succeed()) + for _, app := range appList.Items { + // Remove finalizer to allow deletion + if len(app.Finalizers) > 0 { + app.Finalizers = nil + Expect(k8sClient.Update(ctx, &app)).To(Succeed()) + } + Expect(k8sClient.Delete(ctx, &app)).To(Succeed()) + } + + // Cleanup other resources that might have been created + // We can add more cleanup here if needed, but deleting Applications should be enough + // if owner references are set (though they might not be in these manual tests). + }) + + It("should successfully reconcile a Deployment", func() { + resourceName := "test-deployment" + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + + By("creating the custom resource for the Kind Application") + resource := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: apiv2.ApplicationSpec{ + Kind: "Deployment", + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-container", + Image: "nginx", + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + controllerReconciler := &ApplicationReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + By("Reconciling the created resource to add finalizer") + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + By("Reconciling again to create the Deployment") + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + By("Checking if the Deployment was created") + foundDeployment := &appsv1.Deployment{} + err = k8sClient.Get(ctx, typeNamespacedName, foundDeployment) + Expect(err).NotTo(HaveOccurred()) + Expect(foundDeployment.Name).To(Equal(resourceName)) + Expect(foundDeployment.Spec.Template.Spec.Containers[0].Image).To(Equal("nginx")) + }) + + It("should successfully scale a Deployment using Replicas field", func() { + resourceName := "test-replicas" + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + var replicas int32 = 3 + + resource := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: apiv2.ApplicationSpec{ + Kind: "Deployment", + Replicas: &replicas, + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-container", + Image: "nginx", + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + controllerReconciler := &ApplicationReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // Reconcile to add finalizer + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + // Reconcile to create Deployment + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + foundDeployment := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, typeNamespacedName, foundDeployment)).To(Succeed()) + Expect(*foundDeployment.Spec.Replicas).To(Equal(replicas)) + + // Update replicas + var newReplicas int32 = 5 + Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) + resource.Spec.Replicas = &newReplicas + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + Expect(k8sClient.Get(ctx, typeNamespacedName, foundDeployment)).To(Succeed()) + Expect(*foundDeployment.Spec.Replicas).To(Equal(newReplicas)) + }) + + It("should successfully create and manage an HPA", func() { + resourceName := "test-hpa" + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + var minReplicas int32 = 2 + var maxReplicas int32 = 10 + + resource := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: apiv2.ApplicationSpec{ + Kind: "Deployment", + HpaTemplate: &autoscalingv2.HorizontalPodAutoscalerSpec{ + MinReplicas: &minReplicas, + MaxReplicas: maxReplicas, + }, + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-container", + Image: "nginx", + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + controllerReconciler := &ApplicationReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // Reconcile to add finalizer + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + // Reconcile to create Deployment and HPA + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + // Check Deployment replicas (should be set to MinReplicas on creation) + foundDeployment := &appsv1.Deployment{} + Expect(k8sClient.Get(ctx, typeNamespacedName, foundDeployment)).To(Succeed()) + Expect(*foundDeployment.Spec.Replicas).To(Equal(minReplicas)) + + // Check HPA + foundHPA := &autoscalingv2.HorizontalPodAutoscaler{} + Expect(k8sClient.Get(ctx, typeNamespacedName, foundHPA)).To(Succeed()) + Expect(*foundHPA.Spec.MinReplicas).To(Equal(minReplicas)) + Expect(foundHPA.Spec.MaxReplicas).To(Equal(maxReplicas)) + Expect(foundHPA.Spec.ScaleTargetRef.Kind).To(Equal("Deployment")) + Expect(foundHPA.Spec.ScaleTargetRef.Name).To(Equal(resourceName)) + + // Remove HPA template and set manual replicas + var replicas int32 = 4 + Expect(k8sClient.Get(ctx, typeNamespacedName, resource)).To(Succeed()) + resource.Spec.HpaTemplate = nil + resource.Spec.Replicas = &replicas + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + + // Reconcile again + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + // HPA should be deleted + err = k8sClient.Get(ctx, typeNamespacedName, foundHPA) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + + // Deployment replicas should be updated + Expect(k8sClient.Get(ctx, typeNamespacedName, foundDeployment)).To(Succeed()) + Expect(*foundDeployment.Spec.Replicas).To(Equal(replicas)) + }) + + It("should successfully reconcile a StatefulSet", func() { + resourceName := "test-statefulset" + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + + resource := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: apiv2.ApplicationSpec{ + Kind: "StatefulSet", + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-container", + Image: "redis", + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + controllerReconciler := &ApplicationReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // 1. Add finalizer + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + // 2. Create StatefulSet + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + found := &appsv1.StatefulSet{} + err = k8sClient.Get(ctx, typeNamespacedName, found) + Expect(err).NotTo(HaveOccurred()) + Expect(found.Spec.Template.Spec.Containers[0].Image).To(Equal("redis")) + }) + + It("should propagate VolumeClaimTemplates to the StatefulSet", func() { + resourceName := "test-statefulset-pvc" + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + + resource := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: apiv2.ApplicationSpec{ + Kind: "StatefulSet", + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "etcd", + Image: "etcd", + VolumeMounts: []corev1.VolumeMount{ + {Name: "data", MountPath: "/data"}, + }, + }, + }, + }, + }, + VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{Name: "data"}, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("5Gi")}, + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + controllerReconciler := &ApplicationReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + found := &appsv1.StatefulSet{} + err = k8sClient.Get(ctx, typeNamespacedName, found) + Expect(err).NotTo(HaveOccurred()) + Expect(found.Spec.VolumeClaimTemplates).To(HaveLen(1)) + Expect(found.Spec.VolumeClaimTemplates[0].Name).To(Equal("data")) + }) + + It("should successfully reconcile a Rollout", func() { + resourceName := "test-rollout" + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + + resource := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: apiv2.ApplicationSpec{ + Kind: "Rollout", + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-container", + Image: "nginx:latest", + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + controllerReconciler := &ApplicationReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // 1. Add finalizer + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + // 2. Create Rollout + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + found := &v1alpha1.Rollout{} + err = k8sClient.Get(ctx, typeNamespacedName, found) + Expect(err).NotTo(HaveOccurred()) + Expect(found.Spec.Template.Spec.Containers[0].Image).To(Equal("nginx:latest")) + }) + + It("should successfully reconcile a DaemonSet", func() { + resourceName := "test-daemonset" + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + + resource := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: apiv2.ApplicationSpec{ + Kind: "DaemonSet", + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test-container", + Image: "fluentd", + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + controllerReconciler := &ApplicationReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // 1. Add finalizer + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + // 2. Reconcile (DaemonSet is currently a no-op in the controller) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + By("DaemonSet is currently not implemented, so we just verify reconciliation succeeds") + }) + + It("should successfully reconcile a Service", func() { + resourceName := "test-service" + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + + resource := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: apiv2.ApplicationSpec{ + Kind: "Deployment", + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "web", Image: "nginx"}}, + }, + }, + ServiceTemplate: &corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: 80, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + controllerReconciler := &ApplicationReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // 1. Add finalizer + _, reconcileErr := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(reconcileErr).NotTo(HaveOccurred()) + // 2. Create Deployment and Service + _, reconcileErr = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(reconcileErr).NotTo(HaveOccurred()) + + found := &corev1.Service{} + err := k8sClient.Get(ctx, typeNamespacedName, found) + Expect(err).NotTo(HaveOccurred()) + Expect(found.Spec.Ports[0].Port).To(Equal(int32(80))) + }) + + It("round-trips a normalized ServiceTemplate without drift", func() { + // The Application CRD schema defaults serviceTemplate.ports[].protocol, + // so an un-normalized template reads back different from what was + // written — the drift that made the parent's update gate fire on every + // reconcile and kept Service-bearing Applications churning. + raw := &corev1.ServiceSpec{Ports: []corev1.ServicePort{{Name: "http", Port: 8080}}} + + rawApp := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{Name: "test-roundtrip-raw", Namespace: "default"}, + Spec: apiv2.ApplicationSpec{ + Kind: "Deployment", + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "web", Image: "nginx"}}}, + }, + ServiceTemplate: raw.DeepCopy(), + }, + } + Expect(k8sClient.Create(ctx, rawApp)).To(Succeed()) + + fetched := &apiv2.Application{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test-roundtrip-raw", Namespace: "default"}, fetched)).To(Succeed()) + Expect(fetched.Spec.ServiceTemplate.Ports[0].Protocol).To(Equal(corev1.ProtocolTCP), + "CRD schema defaulting fills ports[].protocol") + Expect(apiequality.Semantic.DeepEqual(fetched.Spec.ServiceTemplate, raw)).To(BeFalse(), + "un-normalized templates do not round-trip; reconcilers must not build them") + + // The normalized form (what reconcileApplications writes now) is stable. + normalized := raw.DeepCopy() + common.NormalizeServicePorts(normalized.Ports) + normApp := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{Name: "test-roundtrip-norm", Namespace: "default"}, + Spec: apiv2.ApplicationSpec{ + Kind: "Deployment", + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "web", Image: "nginx"}}}, + }, + ServiceTemplate: normalized.DeepCopy(), + }, + } + Expect(k8sClient.Create(ctx, normApp)).To(Succeed()) + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test-roundtrip-norm", Namespace: "default"}, fetched)).To(Succeed()) + Expect(apiequality.Semantic.DeepEqual(fetched.Spec.ServiceTemplate, normalized)).To(BeTrue(), + "normalized templates round-trip unchanged, so the update gate settles") + }) + + It("does not update the Service on a steady-state reconcile", func() { + resourceName := "test-service-steady" + typeNamespacedName := types.NamespacedName{Name: resourceName, Namespace: "default"} + + resource := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{Name: resourceName, Namespace: "default"}, + Spec: apiv2.ApplicationSpec{ + Kind: "Deployment", + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "web", Image: "nginx"}}}, + }, + // No protocol/targetPort: mirrors templates written before + // normalization existed. + ServiceTemplate: &corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Name: "http", Port: 80}}, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + // Count Service Update calls: the API server absorbs writes of + // defaulted-back fields as no-ops (resourceVersion holds), but each + // call still fires the Owns(Service) watch and re-queues the + // Application — the hot loop this test pins. + counter := &serviceUpdateCounter{Client: k8sClient} + controllerReconciler := &ApplicationReconciler{Client: counter, Scheme: k8sClient.Scheme()} + + // 1. Add finalizer; 2. create Deployment and Service. + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + svc := &corev1.Service{} + Expect(k8sClient.Get(ctx, typeNamespacedName, svc)).To(Succeed()) + + // Steady state: the API server has defaulted fields the template leaves + // unset (protocol, targetPort, sessionAffinity, type, ...); reconciling + // again must not write the Service at all. + counter.updates = 0 + for i := 0; i < 3; i++ { + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + } + Expect(counter.updates).To(BeZero(), + "steady-state reconciles must not update the Service") + }) + + It("should successfully reconcile Jobs and CronJobs", func() { + resourceName := "test-jobs" + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + + resource := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: apiv2.ApplicationSpec{ + Kind: "Deployment", + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "web", Image: "nginx"}}, + }, + }, + Jobs: []batchv1.Job{ + { + ObjectMeta: metav1.ObjectMeta{Name: "test-job"}, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + Containers: []corev1.Container{{Name: "job", Image: "busybox"}}, + }, + }, + }, + }, + }, + CronJobs: []batchv1.CronJob{ + { + ObjectMeta: metav1.ObjectMeta{Name: "test-cronjob"}, + Spec: batchv1.CronJobSpec{ + Schedule: "* * * * *", + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + Containers: []corev1.Container{{Name: "cronjob", Image: "busybox"}}, + }, + }, + }, + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + controllerReconciler := &ApplicationReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // 1. Add finalizer + _, reconcileErr := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(reconcileErr).NotTo(HaveOccurred()) + // 2. Create Resources + _, reconcileErr = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(reconcileErr).NotTo(HaveOccurred()) + + By("Checking the Job") + foundJob := &batchv1.Job{} + err := k8sClient.Get(ctx, types.NamespacedName{Name: "test-job", Namespace: "default"}, foundJob) + Expect(err).NotTo(HaveOccurred()) + + By("Checking the CronJob") + foundCronJob := &batchv1.CronJob{} + err = k8sClient.Get(ctx, types.NamespacedName{Name: "test-cronjob", Namespace: "default"}, foundCronJob) + Expect(err).NotTo(HaveOccurred()) + }) + It("should successfully delete resources when Application is deleted", func() { + resourceName := "test-deletion" + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + + resource := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: apiv2.ApplicationSpec{ + Kind: "Deployment", + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "web", Image: "nginx"}}, + }, + }, + ServiceTemplate: &corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 80}}, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + controllerReconciler := &ApplicationReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // 1. Add finalizer + _, reconcileErr := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(reconcileErr).NotTo(HaveOccurred()) + // 2. Create Resources + _, reconcileErr = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(reconcileErr).NotTo(HaveOccurred()) + + // Verify they exist + Expect(k8sClient.Get(ctx, typeNamespacedName, &appsv1.Deployment{})).To(Succeed()) + Expect(k8sClient.Get(ctx, typeNamespacedName, &corev1.Service{})).To(Succeed()) + + // 3. Delete Application + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + + // 4. Reconcile deletion (Finalizer handling) + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + + // Verify resources are gone + Expect(apierrors.IsNotFound(k8sClient.Get(ctx, typeNamespacedName, &appsv1.Deployment{}))).To(BeTrue()) + Expect(apierrors.IsNotFound(k8sClient.Get(ctx, typeNamespacedName, &corev1.Service{}))).To(BeTrue()) + + // Verify Application is gone (finalizer removed) + err = k8sClient.Get(ctx, typeNamespacedName, &apiv2.Application{}) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }) + }) +}) diff --git a/internal/controller/application_http_route_test.go b/internal/controller/application_http_route_test.go new file mode 100644 index 00000000..fe88405f --- /dev/null +++ b/internal/controller/application_http_route_test.go @@ -0,0 +1,70 @@ +package controller + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + wandbv2 "github.com/wandb/operator/api/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +var _ = Describe("Application HTTPRoute Helpers", func() { + It("builds HTTPRoute rules from exact-match paths and backend ports", func() { + servicePort := gatewayv1.PortNumber(8080) + app := &wandbv2.Application{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb-api"}, + Spec: wandbv2.ApplicationSpec{ + HTTPRouteTemplate: &wandbv2.HTTPRouteTemplateSpec{ + Paths: []string{"/api", "/graphql"}, + PathType: "Exact", + ServicePort: &servicePort, + }, + }, + } + + rules := buildHTTPRouteRules(app) + + Expect(rules).To(HaveLen(1)) + Expect(rules[0].Matches).To(HaveLen(2)) + Expect(*rules[0].Matches[0].Path.Type).To(Equal(gatewayv1.PathMatchExact)) + Expect(*rules[0].Matches[0].Path.Value).To(Equal("/api")) + Expect(*rules[0].Matches[1].Path.Type).To(Equal(gatewayv1.PathMatchExact)) + Expect(*rules[0].Matches[1].Path.Value).To(Equal("/graphql")) + Expect(rules[0].BackendRefs).To(HaveLen(1)) + Expect(rules[0].BackendRefs[0].Name).To(Equal(gatewayv1.ObjectName("wandb-api"))) + Expect(rules[0].BackendRefs[0].Port).NotTo(BeNil()) + Expect(*rules[0].BackendRefs[0].Port).To(Equal(gatewayv1.PortNumber(8080))) + }) + + It("marks an HTTPRoute accepted when any parent reports Accepted=True", func() { + route := &gatewayv1.HTTPRoute{ + Status: gatewayv1.HTTPRouteStatus{ + RouteStatus: gatewayv1.RouteStatus{ + Parents: []gatewayv1.RouteParentStatus{ + { + Conditions: []metav1.Condition{ + { + Type: string(gatewayv1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + }, + }, + }, + { + Conditions: []metav1.Condition{ + { + Type: string(gatewayv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }, + }, + }, + } + + summary := summarizeHTTPRouteStatus(route) + + Expect(summary).NotTo(BeNil()) + Expect(summary.Accepted).To(BeTrue()) + }) +}) diff --git a/internal/controller/application_service_test.go b/internal/controller/application_service_test.go new file mode 100644 index 00000000..8e372c3a --- /dev/null +++ b/internal/controller/application_service_test.go @@ -0,0 +1,94 @@ +package controller + +import ( + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/wandb/operator/internal/controller/common" +) + +// defaultedClusterIPSpec is what the API server stores for a Service created +// from a template that only names a port. +func defaultedClusterIPSpec() *corev1.ServiceSpec { + itp := corev1.ServiceInternalTrafficPolicyCluster + return &corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "10.0.0.1", + SessionAffinity: corev1.ServiceAffinityNone, + InternalTrafficPolicy: &itp, + Ports: []corev1.ServicePort{ + {Name: "http", Port: 80, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(80)}, + }, + } +} + +func TestPreserveServerDefaultedServiceFields_SteadyStateSettles(t *testing.T) { + current := defaultedClusterIPSpec() + desired := &corev1.ServiceSpec{Ports: []corev1.ServicePort{{Name: "http", Port: 80}}} + + common.NormalizeServicePorts(desired.Ports) + preserveServerDefaultedServiceFields(desired, current) + + require.Equal(t, current.Type, desired.Type) + require.Equal(t, current.SessionAffinity, desired.SessionAffinity) + require.Equal(t, current.InternalTrafficPolicy, desired.InternalTrafficPolicy) + require.True(t, apiequality.Semantic.DeepEqual(current.Ports, desired.Ports), + "a template-derived spec must compare equal to the server-defaulted one at steady state") +} + +func TestPreserveServerDefaultedServiceFields_RealChangesStillDiffer(t *testing.T) { + current := defaultedClusterIPSpec() + desired := &corev1.ServiceSpec{Ports: []corev1.ServicePort{{Name: "http", Port: 9090}}} + + common.NormalizeServicePorts(desired.Ports) + preserveServerDefaultedServiceFields(desired, current) + + require.False(t, apiequality.Semantic.DeepEqual(current.Ports, desired.Ports), + "a genuine port change must still be detected") +} + +func TestPreserveServerDefaultedServiceFields_NodePorts(t *testing.T) { + current := &corev1.ServiceSpec{ + Type: corev1.ServiceTypeNodePort, + Ports: []corev1.ServicePort{ + {Name: "http", Port: 80, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(80), NodePort: 30080}, + }, + } + + desired := &corev1.ServiceSpec{ + Type: corev1.ServiceTypeNodePort, + Ports: []corev1.ServicePort{{Name: "http", Port: 80}}, + } + common.NormalizeServicePorts(desired.Ports) + preserveServerDefaultedServiceFields(desired, current) + require.Equal(t, int32(30080), desired.Ports[0].NodePort, "allocated NodePort is preserved") + + pinned := &corev1.ServiceSpec{ + Type: corev1.ServiceTypeNodePort, + Ports: []corev1.ServicePort{{Name: "http", Port: 80, NodePort: 31000}}, + } + common.NormalizeServicePorts(pinned.Ports) + preserveServerDefaultedServiceFields(pinned, current) + require.Equal(t, int32(31000), pinned.Ports[0].NodePort, "template-pinned NodePort wins") +} + +func TestPreserveServerDefaultedServiceFields_ClusterIPDropsNodePorts(t *testing.T) { + current := &corev1.ServiceSpec{ + Type: corev1.ServiceTypeNodePort, + Ports: []corev1.ServicePort{ + {Name: "http", Port: 80, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(80), NodePort: 30080}, + }, + } + desired := &corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Ports: []corev1.ServicePort{{Name: "http", Port: 80}}, + } + common.NormalizeServicePorts(desired.Ports) + preserveServerDefaultedServiceFields(desired, current) + require.Zero(t, desired.Ports[0].NodePort, + "switching to ClusterIP must not carry the stale NodePort") +} diff --git a/internal/controller/common/co_managed.go b/internal/controller/common/co_managed.go new file mode 100644 index 00000000..4adce0a2 --- /dev/null +++ b/internal/controller/common/co_managed.go @@ -0,0 +1,74 @@ +package common + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// WriteOwnedFields create-or-updates obj, writing ONLY the fields the operator +// owns and preserving everything a co-managing controller writes (finalizers, +// status, annotations). +// +// Use it for vendored CRs (e.g. the Altinity CHI/CHK) whose status holds +// unexported fields and uint64 values: controllerutil.CreateOrUpdate +// (apiequality.Semantic.DeepEqual) and CreateOrPatch (DeepCopyJSON of the +// status) both panic traversing that status. Here we fetch, mutate only owned +// fields, compare only those, and Update — which marshals via JSON and never +// deep-copies the status. +// +// applyOwned mutates the fetched object in place. ownedEqual must compare only +// owned fields (spec, labels, owner refs); excluding status keeps the +// panic-prone vendored status out of the comparison. +func WriteOwnedFields[T client.Object]( + ctx context.Context, + cl client.Client, + obj T, + applyOwned func(obj T), + ownedEqual func(a, b T) bool, +) (controllerutil.OperationResult, error) { + key := client.ObjectKeyFromObject(obj) + if err := cl.Get(ctx, key, obj); err != nil { + if !apierrors.IsNotFound(err) { + return controllerutil.OperationResultNone, err + } + obj.SetName(key.Name) + obj.SetNamespace(key.Namespace) + applyOwned(obj) + if err := cl.Create(ctx, obj); err != nil { + return controllerutil.OperationResultNone, err + } + return controllerutil.OperationResultCreated, nil + } + + before, ok := obj.DeepCopyObject().(T) + if !ok { + return controllerutil.OperationResultNone, fmt.Errorf("DeepCopyObject returned unexpected type for %T", obj) + } + applyOwned(obj) + if ownedEqual(before, obj) { + return controllerutil.OperationResultNone, nil + } + if err := cl.Update(ctx, obj); err != nil { + return controllerutil.OperationResultNone, err + } + return controllerutil.OperationResultUpdated, nil +} + +// JSONEqual reports whether a and b marshal to identical JSON. It is panic-safe +// for vendored types whose reflective deep-equal/deep-copy chokes on unexported +// or uint64 fields (json.Marshal skips unexported fields and accepts uint64). A +// marshal error is treated as not-equal so the caller writes rather than skips. +func JSONEqual(a, b any) bool { + ab, err1 := json.Marshal(a) + bb, err2 := json.Marshal(b) + if err1 != nil || err2 != nil { + return false + } + return bytes.Equal(ab, bb) +} diff --git a/internal/controller/common/common_suite_test.go b/internal/controller/common/common_suite_test.go new file mode 100644 index 00000000..9f344489 --- /dev/null +++ b/internal/controller/common/common_suite_test.go @@ -0,0 +1,13 @@ +package common + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestCommon(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Common Suite") +} diff --git a/internal/controller/common/condition.go b/internal/controller/common/condition.go new file mode 100644 index 00000000..091f8eae --- /dev/null +++ b/internal/controller/common/condition.go @@ -0,0 +1,139 @@ +package common + +import ( + "time" + + "github.com/samber/lo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + PendingDeleteReason = "PendingDelete" + PendingCreateReason = "PendingCreate" + ResourceExistsReason = "ResourceExists" + NoResourceReason = "NoResource" + ReportedStatusReason = "ReportedStatus" + ApiErrorReason = "ApiError" + ControllerErrorReason = "ControllerError" + ResourceErrorReason = "ResourceError" + UnknownReason = "Unknown" + DetachedSpecMismatch = "DetachedSpecMismatch" + InvalidNameReason = "InvalidName" +) + +const ( + ReconciledType = "Reconciled" +) + +const DefaultConditionExpiry = 2 * time.Hour + +func ContainsType(conditions []metav1.Condition, typeName string) bool { + return lo.ContainsBy(conditions, func(c metav1.Condition) bool { + return c.Type == typeName + }) +} + +// ComputeConditionUpdates is a complex flow that: +// 1. filters out duplicate(older) conditions of the same Type +// 2. prepares the currentConditions for reconciliation with currentGeneration and LastTransitionTime +// 3. supply conditions whereby: +// a. keep the old condition if there's no new condition of that type +// b. use the new condition if there's no old condition of that type +// c. update the condition of the new condition has changed data +// 4. filter out expired conditions +func ComputeConditionUpdates( + oldConditions, currentConditions []metav1.Condition, + currentGeneration int64, + expiry time.Duration, +) []metav1.Condition { + // filter out any conditions with duplicated types by taking the last(latest) of each Type for old & current + oldConditions = takeLatestByType(oldConditions) + currentConditions = takeLatestByType(currentConditions) + + // give each current a lastReconciled of Now() and the current reconcile Generation + currentConditions = setToCurrentReconciliation(currentConditions, currentGeneration) + + // Unique set of Condition types: from old or current + typeSet := lo.Uniq(lo.Map( + append(oldConditions, currentConditions...), + func(c metav1.Condition, _ int) string { return c.Type }), + ) + + // Find old & current Condition for each Type + type matchedTypes struct { + old *metav1.Condition + current *metav1.Condition + } + oldPointers := lo.Map(oldConditions, func(c metav1.Condition, _ int) *metav1.Condition { return &c }) + currentPointers := lo.Map(currentConditions, func(c metav1.Condition, _ int) *metav1.Condition { return &c }) + possibleUpdates := make([]matchedTypes, 0, len(typeSet)) + for _, typeName := range typeSet { + old := lo.FindOrElse(oldPointers, nil, func(c *metav1.Condition) bool { return c.Type == typeName }) + current := lo.FindOrElse(currentPointers, nil, func(c *metav1.Condition) bool { return c.Type == typeName }) + possibleUpdates = append(possibleUpdates, matchedTypes{old, current}) + } + + // for each old/current of a given Type: + // - use old, if there is no current + // - use current, if there is no old + // - prefer the current to the old *only* if it has updates + // (so that the lastTransitionTime and ObservedGeneration are only updated when there are changes) + result := make([]metav1.Condition, 0) + for _, upd := range possibleUpdates { + if upd.old == nil && upd.current == nil { + break // this should never happen, but for completeness... + } + if upd.old == nil { + result = append(result, *upd.current) + } else if upd.current == nil { + result = append(result, *upd.old) + } else { + if hasConditionChanged(*upd.old, *upd.current) { + result = append(result, *upd.current) + } else { + result = append(result, *upd.old) + } + } + } + + // for conditions that haven't been updated in a long time, perhaps due to some infra update, + // clean them out so that they don't stay around forever + result = removeExpiredConditions(result, expiry) + + return result +} + +// removeExpiredConditions will exclude Conditions that haven't been updated since expiry time ago +func removeExpiredConditions(result []metav1.Condition, expiry time.Duration) []metav1.Condition { + return lo.Filter(result, func(c metav1.Condition, _ int) bool { + return c.LastTransitionTime.After(time.Now().Add(-expiry)) + }) +} + +// setToCurrentReconciliation should set each condition to the supplied currentGeneration and +// the LastTransitionTime to Now() +// This indicates that the condition has been updated during this reconcile loop. +func setToCurrentReconciliation(conditions []metav1.Condition, currentGeneration int64) []metav1.Condition { + return lo.Map(conditions, func(c metav1.Condition, _ int) metav1.Condition { + c.LastTransitionTime = metav1.Now() + c.ObservedGeneration = currentGeneration + return c + }) +} + +// hasConditionChanged looks for relevant field changes, excluding LastTransitionTime. +// It assumes old and current are of the same Type. +func hasConditionChanged(old metav1.Condition, current metav1.Condition) bool { + return old.Status != current.Status || old.Message != current.Message || old.Reason != current.Reason +} + +// takeLatestByType will filter out conditions so that only the last for each Type +// combination will be returned. Sometimes, more information about a Type may become +// available during a single pass of a reconciliation loop. +func takeLatestByType(conditions []metav1.Condition) []metav1.Condition { + registry := make(map[string]metav1.Condition) + for _, c := range conditions { + registry[c.Type] = c + } + return lo.Values(registry) +} diff --git a/internal/controller/common/condition_test.go b/internal/controller/common/condition_test.go new file mode 100644 index 00000000..69e7f49b --- /dev/null +++ b/internal/controller/common/condition_test.go @@ -0,0 +1,940 @@ +package common + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("Condition", func() { + Describe("ComputeConditionUpdates", func() { + var ( + currentGeneration int64 + expiry time.Duration + ) + + BeforeEach(func() { + currentGeneration = int64(10) + expiry = 1 * time.Hour + }) + + It("should return empty slice when both old and current conditions are empty", func() { + oldConditions := []metav1.Condition{} + currentConditions := []metav1.Condition{} + + result := ComputeConditionUpdates(oldConditions, currentConditions, currentGeneration, expiry) + + Expect(result).To(HaveLen(0)) + }) + + It("should keep old conditions when no current conditions are provided", func() { + oldTime := metav1.NewTime(time.Now().Add(-10 * time.Minute)) + oldConditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + LastTransitionTime: oldTime, + Reason: "OldReason1", + Message: "Old message 1", + }, + { + Type: "Type2", + Status: metav1.ConditionFalse, + ObservedGeneration: 5, + LastTransitionTime: oldTime, + Reason: "OldReason2", + Message: "Old message 2", + }, + } + currentConditions := []metav1.Condition{} + + result := ComputeConditionUpdates(oldConditions, currentConditions, currentGeneration, expiry) + + Expect(result).To(HaveLen(2)) + // TODO This fails because the order appears to change, not sure if the problem is the code or the test + //Expect(result[0].Reason).To(Equal(oldConditions[0].Reason)) + //Expect(result[1].Reason).To(Equal(oldConditions[1].Reason)) + Expect(result[0].ObservedGeneration).To(Equal(int64(5))) + Expect(result[1].ObservedGeneration).To(Equal(int64(5))) + }) + + It("should use current conditions when no old conditions exist", func() { + oldConditions := []metav1.Condition{} + currentConditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + Reason: "NewReason1", + Message: "New message 1", + }, + { + Type: "Type2", + Status: metav1.ConditionFalse, + Reason: "NewReason2", + Message: "New message 2", + }, + } + + result := ComputeConditionUpdates(oldConditions, currentConditions, currentGeneration, expiry) + + Expect(result).To(HaveLen(2)) + Expect(result).To(ConsistOf( + And( + HaveField("Type", "Type1"), + HaveField("Reason", "NewReason1"), + HaveField("ObservedGeneration", currentGeneration), + ), + And( + HaveField("Type", "Type2"), + HaveField("Reason", "NewReason2"), + HaveField("ObservedGeneration", currentGeneration), + ), + )) + }) + + It("should keep old condition when current has no changes", func() { + oldTime := metav1.NewTime(time.Now().Add(-10 * time.Minute)) + oldConditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + LastTransitionTime: oldTime, + Reason: "Reason1", + Message: "Message 1", + }, + } + currentConditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + Reason: "Reason1", + Message: "Message 1", + }, + } + + result := ComputeConditionUpdates(oldConditions, currentConditions, currentGeneration, expiry) + + Expect(result).To(HaveLen(1)) + Expect(result[0].LastTransitionTime).To(Equal(oldTime)) + Expect(result[0].ObservedGeneration).To(Equal(int64(5))) + }) + + It("should update to current condition when Status changes", func() { + oldTime := metav1.NewTime(time.Now().Add(-10 * time.Minute)) + oldConditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + LastTransitionTime: oldTime, + Reason: "Reason1", + Message: "Message 1", + }, + } + currentConditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionFalse, + Reason: "Reason1", + Message: "Message 1", + }, + } + + result := ComputeConditionUpdates(oldConditions, currentConditions, currentGeneration, expiry) + + Expect(result).To(HaveLen(1)) + Expect(result[0].Status).To(Equal(metav1.ConditionFalse)) + Expect(result[0].ObservedGeneration).To(Equal(currentGeneration)) + Expect(result[0].LastTransitionTime.Time).To(BeTemporally("~", time.Now(), 1*time.Second)) + }) + + It("should update to current condition when Reason changes", func() { + oldTime := metav1.NewTime(time.Now().Add(-10 * time.Minute)) + oldConditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + LastTransitionTime: oldTime, + Reason: "OldReason", + Message: "Message 1", + }, + } + currentConditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + Reason: "NewReason", + Message: "Message 1", + }, + } + + result := ComputeConditionUpdates(oldConditions, currentConditions, currentGeneration, expiry) + + Expect(result).To(HaveLen(1)) + Expect(result[0].Reason).To(Equal("NewReason")) + Expect(result[0].ObservedGeneration).To(Equal(currentGeneration)) + Expect(result[0].LastTransitionTime.Time).To(BeTemporally("~", time.Now(), 1*time.Second)) + }) + + It("should update to current condition when Message changes", func() { + oldTime := metav1.NewTime(time.Now().Add(-10 * time.Minute)) + oldConditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + LastTransitionTime: oldTime, + Reason: "Reason1", + Message: "Old message", + }, + } + currentConditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + Reason: "Reason1", + Message: "New message", + }, + } + + result := ComputeConditionUpdates(oldConditions, currentConditions, currentGeneration, expiry) + + Expect(result).To(HaveLen(1)) + Expect(result[0].Message).To(Equal("New message")) + Expect(result[0].ObservedGeneration).To(Equal(currentGeneration)) + Expect(result[0].LastTransitionTime.Time).To(BeTemporally("~", time.Now(), 1*time.Second)) + }) + + It("should handle mix of old-only, current-only, and shared types", func() { + oldTime := metav1.NewTime(time.Now().Add(-10 * time.Minute)) + oldConditions := []metav1.Condition{ + { + Type: "OldOnly", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + LastTransitionTime: oldTime, + Reason: "OldReason", + Message: "This only exists in old", + }, + { + Type: "Shared", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + LastTransitionTime: oldTime, + Reason: "SharedReason", + Message: "Shared condition", + }, + } + currentConditions := []metav1.Condition{ + { + Type: "CurrentOnly", + Status: metav1.ConditionFalse, + Reason: "NewReason", + Message: "This only exists in current", + }, + { + Type: "Shared", + Status: metav1.ConditionTrue, + Reason: "SharedReason", + Message: "Shared condition", + }, + } + + result := ComputeConditionUpdates(oldConditions, currentConditions, currentGeneration, expiry) + + Expect(result).To(HaveLen(3)) + + oldOnlyCondition := findConditionByType(result, "OldOnly") + Expect(oldOnlyCondition).NotTo(BeNil()) + Expect(oldOnlyCondition.ObservedGeneration).To(Equal(int64(5))) + + currentOnlyCondition := findConditionByType(result, "CurrentOnly") + Expect(currentOnlyCondition).NotTo(BeNil()) + Expect(currentOnlyCondition.ObservedGeneration).To(Equal(currentGeneration)) + + sharedCondition := findConditionByType(result, "Shared") + Expect(sharedCondition).NotTo(BeNil()) + Expect(sharedCondition.ObservedGeneration).To(Equal(int64(5))) + Expect(sharedCondition.LastTransitionTime).To(Equal(oldTime)) + }) + + It("should deduplicate conditions by taking latest when duplicates exist", func() { + recentTime := metav1.NewTime(time.Now().Add(-10 * time.Minute)) + oldConditions := []metav1.Condition{ + { + Type: "DuplicateType", + Status: metav1.ConditionTrue, + LastTransitionTime: recentTime, + Reason: "First", + Message: "This should be ignored", + }, + { + Type: "DuplicateType", + Status: metav1.ConditionFalse, + LastTransitionTime: recentTime, + Reason: "Second", + Message: "This should be kept", + }, + } + currentConditions := []metav1.Condition{} + + result := ComputeConditionUpdates(oldConditions, currentConditions, currentGeneration, expiry) + + Expect(result).To(HaveLen(1)) + Expect(result[0].Reason).To(Equal("Second")) + Expect(result[0].Message).To(Equal("This should be kept")) + }) + + It("should remove expired conditions", func() { + twoHoursAgo := metav1.NewTime(time.Now().Add(-2 * time.Hour)) + thirtyMinutesAgo := metav1.NewTime(time.Now().Add(-30 * time.Minute)) + + oldConditions := []metav1.Condition{ + { + Type: "ExpiredType", + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + LastTransitionTime: twoHoursAgo, + Reason: "ExpiredReason", + Message: "This should be removed", + }, + { + Type: "ValidType", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + LastTransitionTime: thirtyMinutesAgo, + Reason: "ValidReason", + Message: "This should remain", + }, + } + currentConditions := []metav1.Condition{} + + result := ComputeConditionUpdates(oldConditions, currentConditions, currentGeneration, expiry) + + Expect(result).To(HaveLen(1)) + Expect(result[0].Type).To(Equal("ValidType")) + }) + + It("should handle complex scenario with changes, additions, removals, and expirations", func() { + twoHoursAgo := metav1.NewTime(time.Now().Add(-2 * time.Hour)) + twentyMinutesAgo := metav1.NewTime(time.Now().Add(-20 * time.Minute)) + + oldConditions := []metav1.Condition{ + { + Type: "Unchanged", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + LastTransitionTime: twentyMinutesAgo, + Reason: "UnchangedReason", + Message: "This stays the same", + }, + { + Type: "Changed", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + LastTransitionTime: twentyMinutesAgo, + Reason: "OldReason", + Message: "Old message", + }, + { + Type: "Expired", + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + LastTransitionTime: twoHoursAgo, + Reason: "ExpiredReason", + Message: "This will be removed", + }, + { + Type: "RemovedFromCurrent", + Status: metav1.ConditionFalse, + ObservedGeneration: 5, + LastTransitionTime: twentyMinutesAgo, + Reason: "StillValid", + Message: "Current reconcile doesn't mention this", + }, + } + + currentConditions := []metav1.Condition{ + { + Type: "Unchanged", + Status: metav1.ConditionTrue, + Reason: "UnchangedReason", + Message: "This stays the same", + }, + { + Type: "Changed", + Status: metav1.ConditionFalse, + Reason: "NewReason", + Message: "New message", + }, + { + Type: "AddedInCurrent", + Status: metav1.ConditionTrue, + Reason: "NewCondition", + Message: "This is brand new", + }, + } + + result := ComputeConditionUpdates(oldConditions, currentConditions, currentGeneration, expiry) + + unchangedCondition := findConditionByType(result, "Unchanged") + Expect(unchangedCondition).NotTo(BeNil()) + Expect(unchangedCondition.LastTransitionTime).To(Equal(twentyMinutesAgo)) + Expect(unchangedCondition.ObservedGeneration).To(Equal(int64(5))) + + changedCondition := findConditionByType(result, "Changed") + Expect(changedCondition).NotTo(BeNil()) + Expect(changedCondition.Status).To(Equal(metav1.ConditionFalse)) + Expect(changedCondition.Reason).To(Equal("NewReason")) + Expect(changedCondition.ObservedGeneration).To(Equal(currentGeneration)) + Expect(changedCondition.LastTransitionTime.Time).To(BeTemporally("~", time.Now(), 1*time.Second)) + + expiredCondition := findConditionByType(result, "Expired") + Expect(expiredCondition).To(BeNil()) + + removedCondition := findConditionByType(result, "RemovedFromCurrent") + Expect(removedCondition).NotTo(BeNil()) + Expect(removedCondition.ObservedGeneration).To(Equal(int64(5))) + + addedCondition := findConditionByType(result, "AddedInCurrent") + Expect(addedCondition).NotTo(BeNil()) + Expect(addedCondition.Reason).To(Equal("NewCondition")) + Expect(addedCondition.ObservedGeneration).To(Equal(currentGeneration)) + }) + + It("should handle zero expiry duration", func() { + oldTime := metav1.NewTime(time.Now().Add(-10 * time.Minute)) + oldConditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + LastTransitionTime: oldTime, + Reason: "Reason1", + Message: "Message 1", + }, + } + currentConditions := []metav1.Condition{} + + result := ComputeConditionUpdates(oldConditions, currentConditions, currentGeneration, 0) + + Expect(result).To(HaveLen(0)) + }) + }) + + Describe("ContainsType", func() { + It("should return true when type exists in conditions", func() { + conditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + Reason: "Reason1", + Message: "Message1", + }, + { + Type: "Type2", + Status: metav1.ConditionFalse, + Reason: "Reason2", + Message: "Message2", + }, + { + Type: "Type3", + Status: metav1.ConditionTrue, + Reason: "Reason3", + Message: "Message3", + }, + } + + Expect(ContainsType(conditions, "Type1")).To(BeTrue()) + Expect(ContainsType(conditions, "Type2")).To(BeTrue()) + Expect(ContainsType(conditions, "Type3")).To(BeTrue()) + }) + + It("should return false when type does not exist in conditions", func() { + conditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + Reason: "Reason1", + Message: "Message1", + }, + { + Type: "Type2", + Status: metav1.ConditionFalse, + Reason: "Reason2", + Message: "Message2", + }, + } + + Expect(ContainsType(conditions, "Type3")).To(BeFalse()) + Expect(ContainsType(conditions, "NonExistentType")).To(BeFalse()) + }) + + It("should return false for empty conditions slice", func() { + conditions := []metav1.Condition{} + + Expect(ContainsType(conditions, "Type1")).To(BeFalse()) + }) + + It("should return false for nil conditions slice", func() { + var conditions []metav1.Condition + + Expect(ContainsType(conditions, "Type1")).To(BeFalse()) + }) + + It("should be case-sensitive", func() { + conditions := []metav1.Condition{ + { + Type: "TestType", + Status: metav1.ConditionTrue, + Reason: "Reason", + Message: "Message", + }, + } + + Expect(ContainsType(conditions, "TestType")).To(BeTrue()) + Expect(ContainsType(conditions, "testtype")).To(BeFalse()) + Expect(ContainsType(conditions, "TESTTYPE")).To(BeFalse()) + }) + + It("should handle duplicate types", func() { + conditions := []metav1.Condition{ + { + Type: "DuplicateType", + Status: metav1.ConditionTrue, + Reason: "Reason1", + Message: "Message1", + }, + { + Type: "DuplicateType", + Status: metav1.ConditionFalse, + Reason: "Reason2", + Message: "Message2", + }, + } + + Expect(ContainsType(conditions, "DuplicateType")).To(BeTrue()) + }) + }) + + Describe("removeExpiredConditions", func() { + It("should keep conditions that are not expired", func() { + now := metav1.Now() + conditions := []metav1.Condition{ + { + Type: "TestType1", + Status: metav1.ConditionTrue, + LastTransitionTime: now, + Reason: "TestReason", + Message: "Test message", + }, + { + Type: "TestType2", + Status: metav1.ConditionFalse, + LastTransitionTime: now, + Reason: "TestReason2", + Message: "Test message 2", + }, + } + + result := removeExpiredConditions(conditions, 1*time.Hour) + + Expect(result).To(HaveLen(2)) + Expect(result[0].Type).To(Equal("TestType1")) + Expect(result[1].Type).To(Equal("TestType2")) + }) + + It("should remove conditions that are expired", func() { + twoHoursAgo := metav1.NewTime(time.Now().Add(-2 * time.Hour)) + now := metav1.Now() + + conditions := []metav1.Condition{ + { + Type: "ExpiredType", + Status: metav1.ConditionTrue, + LastTransitionTime: twoHoursAgo, + Reason: "ExpiredReason", + Message: "This should be removed", + }, + { + Type: "ValidType", + Status: metav1.ConditionTrue, + LastTransitionTime: now, + Reason: "ValidReason", + Message: "This should remain", + }, + } + + result := removeExpiredConditions(conditions, 1*time.Hour) + + Expect(result).To(HaveLen(1)) + Expect(result[0].Type).To(Equal("ValidType")) + }) + + It("should remove all conditions when all are expired", func() { + twoHoursAgo := metav1.NewTime(time.Now().Add(-2 * time.Hour)) + + conditions := []metav1.Condition{ + { + Type: "ExpiredType1", + Status: metav1.ConditionTrue, + LastTransitionTime: twoHoursAgo, + Reason: "ExpiredReason", + Message: "Expired 1", + }, + { + Type: "ExpiredType2", + Status: metav1.ConditionFalse, + LastTransitionTime: twoHoursAgo, + Reason: "ExpiredReason", + Message: "Expired 2", + }, + } + + result := removeExpiredConditions(conditions, 1*time.Hour) + + Expect(result).To(HaveLen(0)) + }) + + It("should handle empty conditions slice", func() { + conditions := []metav1.Condition{} + + result := removeExpiredConditions(conditions, 1*time.Hour) + + Expect(result).To(HaveLen(0)) + }) + + It("should keep conditions at the exact expiry boundary", func() { + exactlyOneHourAgo := metav1.NewTime(time.Now().Add(-1 * time.Hour)) + + conditions := []metav1.Condition{ + { + Type: "BoundaryType", + Status: metav1.ConditionTrue, + LastTransitionTime: exactlyOneHourAgo, + Reason: "BoundaryReason", + Message: "At boundary", + }, + } + + result := removeExpiredConditions(conditions, 1*time.Hour) + + Expect(result).To(HaveLen(0)) + }) + }) + + Describe("setToCurrentReconciliation", func() { + It("should update ObservedGeneration and LastTransitionTime for all conditions", func() { + oldTime := metav1.NewTime(time.Now().Add(-1 * time.Hour)) + conditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + LastTransitionTime: oldTime, + Reason: "Reason1", + Message: "Message1", + }, + { + Type: "Type2", + Status: metav1.ConditionFalse, + ObservedGeneration: 3, + LastTransitionTime: oldTime, + Reason: "Reason2", + Message: "Message2", + }, + } + + currentGeneration := int64(10) + result := setToCurrentReconciliation(conditions, currentGeneration) + + Expect(result).To(HaveLen(2)) + for _, c := range result { + Expect(c.ObservedGeneration).To(Equal(currentGeneration)) + Expect(c.LastTransitionTime.Time).To(BeTemporally("~", time.Now(), 1*time.Second)) + } + }) + + It("should preserve other fields while updating generation and time", func() { + oldTime := metav1.NewTime(time.Now().Add(-1 * time.Hour)) + conditions := []metav1.Condition{ + { + Type: "TestType", + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + LastTransitionTime: oldTime, + Reason: "TestReason", + Message: "TestMessage", + }, + } + + currentGeneration := int64(5) + result := setToCurrentReconciliation(conditions, currentGeneration) + + Expect(result).To(HaveLen(1)) + Expect(result[0].Type).To(Equal("TestType")) + Expect(result[0].Status).To(Equal(metav1.ConditionTrue)) + Expect(result[0].Reason).To(Equal("TestReason")) + Expect(result[0].Message).To(Equal("TestMessage")) + Expect(result[0].ObservedGeneration).To(Equal(currentGeneration)) + Expect(result[0].LastTransitionTime.Time).To(BeTemporally("~", time.Now(), 1*time.Second)) + }) + + It("should handle empty conditions slice", func() { + conditions := []metav1.Condition{} + + result := setToCurrentReconciliation(conditions, int64(10)) + + Expect(result).To(HaveLen(0)) + }) + + It("should handle generation 0", func() { + conditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + Reason: "Reason1", + Message: "Message1", + }, + } + + result := setToCurrentReconciliation(conditions, int64(0)) + + Expect(result).To(HaveLen(1)) + Expect(result[0].ObservedGeneration).To(Equal(int64(0))) + }) + }) + + Describe("hasConditionChanged", func() { + baseCondition := metav1.Condition{ + Type: "TestType", + Status: metav1.ConditionTrue, + ObservedGeneration: 5, + LastTransitionTime: metav1.Now(), + Reason: "TestReason", + Message: "TestMessage", + } + + It("should return false when conditions are identical", func() { + old := baseCondition + current := baseCondition + + result := hasConditionChanged(old, current) + + Expect(result).To(BeFalse()) + }) + + It("should return true when Status changes", func() { + old := baseCondition + current := baseCondition + current.Status = metav1.ConditionFalse + + result := hasConditionChanged(old, current) + + Expect(result).To(BeTrue()) + }) + + It("should return true when Message changes", func() { + old := baseCondition + current := baseCondition + current.Message = "DifferentMessage" + + result := hasConditionChanged(old, current) + + Expect(result).To(BeTrue()) + }) + + It("should return true when Reason changes", func() { + old := baseCondition + current := baseCondition + current.Reason = "DifferentReason" + + result := hasConditionChanged(old, current) + + Expect(result).To(BeTrue()) + }) + + It("should return false when only LastTransitionTime changes", func() { + old := baseCondition + current := baseCondition + current.LastTransitionTime = metav1.NewTime(time.Now().Add(1 * time.Hour)) + + result := hasConditionChanged(old, current) + + Expect(result).To(BeFalse()) + }) + + It("should return false when only ObservedGeneration changes", func() { + old := baseCondition + current := baseCondition + current.ObservedGeneration = 10 + + result := hasConditionChanged(old, current) + + Expect(result).To(BeFalse()) + }) + + It("should return false when only Type changes", func() { + old := baseCondition + current := baseCondition + current.Type = "DifferentType" + + result := hasConditionChanged(old, current) + + Expect(result).To(BeFalse()) + }) + + It("should return true when multiple relevant fields change", func() { + old := baseCondition + current := baseCondition + current.Status = metav1.ConditionFalse + current.Message = "DifferentMessage" + current.Reason = "DifferentReason" + + result := hasConditionChanged(old, current) + + Expect(result).To(BeTrue()) + }) + }) + + Describe("takeLatestByType", func() { + It("should keep only the last condition for each Type", func() { + conditions := []metav1.Condition{ + { + Type: "TypeA", + Status: metav1.ConditionTrue, + Reason: "FirstA", + Message: "First A message", + }, + { + Type: "TypeB", + Status: metav1.ConditionTrue, + Reason: "FirstB", + Message: "First B message", + }, + { + Type: "TypeA", + Status: metav1.ConditionFalse, + Reason: "SecondA", + Message: "Second A message", + }, + } + + result := takeLatestByType(conditions) + + Expect(result).To(HaveLen(2)) + + typeACondition := findConditionByType(result, "TypeA") + Expect(typeACondition).NotTo(BeNil()) + Expect(typeACondition.Reason).To(Equal("SecondA")) + Expect(typeACondition.Status).To(Equal(metav1.ConditionFalse)) + + typeBCondition := findConditionByType(result, "TypeB") + Expect(typeBCondition).NotTo(BeNil()) + Expect(typeBCondition.Reason).To(Equal("FirstB")) + }) + + It("should handle conditions with unique types", func() { + conditions := []metav1.Condition{ + { + Type: "Type1", + Status: metav1.ConditionTrue, + Reason: "Reason1", + Message: "Message1", + }, + { + Type: "Type2", + Status: metav1.ConditionFalse, + Reason: "Reason2", + Message: "Message2", + }, + { + Type: "Type3", + Status: metav1.ConditionTrue, + Reason: "Reason3", + Message: "Message3", + }, + } + + result := takeLatestByType(conditions) + + Expect(result).To(HaveLen(3)) + }) + + It("should handle empty conditions slice", func() { + conditions := []metav1.Condition{} + + result := takeLatestByType(conditions) + + Expect(result).To(HaveLen(0)) + }) + + It("should handle multiple duplicates of the same type", func() { + conditions := []metav1.Condition{ + { + Type: "TypeA", + Reason: "First", + Message: "1", + }, + { + Type: "TypeA", + Reason: "Second", + Message: "2", + }, + { + Type: "TypeA", + Reason: "Third", + Message: "3", + }, + { + Type: "TypeA", + Reason: "Fourth", + Message: "4", + }, + } + + result := takeLatestByType(conditions) + + Expect(result).To(HaveLen(1)) + Expect(result[0].Reason).To(Equal("Fourth")) + Expect(result[0].Message).To(Equal("4")) + }) + + It("should handle all conditions having the same type", func() { + conditions := []metav1.Condition{ + { + Type: "SameType", + Status: metav1.ConditionTrue, + Reason: "First", + Message: "First message", + }, + { + Type: "SameType", + Status: metav1.ConditionFalse, + Reason: "Second", + Message: "Second message", + }, + } + + result := takeLatestByType(conditions) + + Expect(result).To(HaveLen(1)) + Expect(result[0].Reason).To(Equal("Second")) + }) + }) +}) + +func findConditionByType(conditions []metav1.Condition, typeName string) *metav1.Condition { + for _, c := range conditions { + if c.Type == typeName { + return &c + } + } + return nil +} diff --git a/internal/controller/common/detach.go b/internal/controller/common/detach.go new file mode 100644 index 00000000..48526191 --- /dev/null +++ b/internal/controller/common/detach.go @@ -0,0 +1,24 @@ +package common + +import ( + "github.com/wandb/operator/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func IsDetached(obj client.Object, ownerUID types.UID) bool { + for _, ref := range obj.GetOwnerReferences() { + if ref.UID == ownerUID { + return false + } + } + return true +} + +func RemoveOwnerReference(obj client.Object, ownerUID types.UID) { + newRefs := utils.FilterFunc(obj.GetOwnerReferences(), func(ref metav1.OwnerReference) bool { + return ref.UID != ownerUID + }) + obj.SetOwnerReferences(newRefs) +} diff --git a/internal/controller/common/labels.go b/internal/controller/common/labels.go new file mode 100644 index 00000000..245a830b --- /dev/null +++ b/internal/controller/common/labels.go @@ -0,0 +1,32 @@ +package common + +import ( + apiv2 "github.com/wandb/operator/api/v2" +) + +const ( + WandbNameLabel = "weightsandbiases.apps.wandb.com/name" + WandbNamespaceLabel = "weightsandbiases.apps.wandb.com/namespace" + WandbComponentLabel = "weightsandbiases.apps.wandb.com/component" +) + +// HasAllLabelKeys reports whether existing contains every key present in desired, +// regardless of value. +func HasAllLabelKeys(existing, desired map[string]string) bool { + for k := range desired { + if _, ok := existing[k]; !ok { + return false + } + } + return true +} + +// BuildWandbLabels returns the standard wandb labels for resources managed +// on behalf of the given WeightsAndBiases CR. +func BuildWandbLabels(wandb *apiv2.WeightsAndBiases, componentName string) map[string]string { + return map[string]string{ + WandbNameLabel: wandb.Name, + WandbNamespaceLabel: wandb.Namespace, + WandbComponentLabel: componentName, + } +} diff --git a/internal/controller/common/naming.go b/internal/controller/common/naming.go new file mode 100644 index 00000000..39c86321 --- /dev/null +++ b/internal/controller/common/naming.go @@ -0,0 +1,54 @@ +package common + +import ( + "crypto/sha256" + "fmt" + "strings" + + apiv2 "github.com/wandb/operator/api/v2" + "k8s.io/apimachinery/pkg/util/validation" +) + +// InstanceBaseName is what default infra names derive from: the CR name, plus +// the instance key for non-default instances. The key sits before the infra +// suffix so suffix-based derivations (Keeper pairing, connection-name trims) +// keep working. +func InstanceBaseName(crName, instanceKey string) string { + if instanceKey == "" || instanceKey == apiv2.DefaultInstanceName { + return crName + } + return crName + "-" + instanceKey +} + +// infraNameHashLen is enough to keep sibling CRs sharing a long prefix from colliding. +const infraNameHashLen = 5 + +// FitDefaultInfraName returns "<crName><suffix>", falling back to a +// deterministic "<prefix>-<hash><suffix>" when that would not be a valid +// DNS-1123 label within budget — any CR name yields a usable default. +func FitDefaultInfraName(crName, suffix string, budget int) string { + plain := crName + suffix + if len(plain) <= budget && len(validation.IsDNS1123Label(plain)) == 0 { + return plain + } + + digest := fmt.Sprintf("%x", sha256.Sum256([]byte(crName)))[:infraNameHashLen] + prefix := sanitizeLabelPrefix(crName, budget-len(suffix)-infraNameHashLen-1) + if prefix == "" { + return digest + suffix + } + return fmt.Sprintf("%s-%s%s", prefix, digest, suffix) +} + +// sanitizeLabelPrefix truncates s to maxLen and strips what can't lead a +// DNS-1123 label (CR names may contain dots; truncation may leave hyphens). +func sanitizeLabelPrefix(s string, maxLen int) string { + if maxLen <= 0 { + return "" + } + s = strings.ReplaceAll(s, ".", "-") + if len(s) > maxLen { + s = s[:maxLen] + } + return strings.Trim(s, "-") +} diff --git a/internal/controller/common/naming_test.go b/internal/controller/common/naming_test.go new file mode 100644 index 00000000..f084fdb0 --- /dev/null +++ b/internal/controller/common/naming_test.go @@ -0,0 +1,49 @@ +package common + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/util/validation" +) + +var _ = Describe("FitDefaultInfraName", func() { + // deliberately tight so the cases exercise shortening + const budget = 27 + + It("keeps the plain name when it fits the budget", func() { + Expect(FitDefaultInfraName("wandb", "-clickhouse", budget)).To(Equal("wandb-clickhouse")) + }) + + It("shortens over-budget names deterministically and keeps the suffix", func() { + first := FitDefaultInfraName("wandb-legacy-overrides-v1", "-clickhouse", budget) + second := FitDefaultInfraName("wandb-legacy-overrides-v1", "-clickhouse", budget) + + Expect(first).To(Equal(second)) + Expect(len(first)).To(BeNumerically("<=", budget)) + Expect(first).To(HaveSuffix("-clickhouse")) + Expect(validation.IsDNS1123Label(first)).To(BeEmpty()) + }) + + It("derives distinct names for distinct CR names sharing a long prefix", func() { + one := FitDefaultInfraName("wandb-production-eu-west-1", "-clickhouse", budget) + two := FitDefaultInfraName("wandb-production-eu-west-2", "-clickhouse", budget) + + Expect(one).NotTo(Equal(two)) + }) + + It("sanitizes dots, which are legal in CR names but not in labels", func() { + name := FitDefaultInfraName("wandb.prod.eu", "-clickhouse", budget) + + Expect(name).NotTo(ContainSubstring(".")) + Expect(len(name)).To(BeNumerically("<=", budget)) + Expect(validation.IsDNS1123Label(name)).To(BeEmpty()) + }) + + It("survives truncation points that land on a hyphen", func() { + // prefix budget is 10 here; the 10th char of the CR name is '-' + name := FitDefaultInfraName("wandb-abcd-something-long-enough", "-clickhouse", budget) + + Expect(name).NotTo(ContainSubstring("--")) + Expect(validation.IsDNS1123Label(name)).To(BeEmpty()) + }) +}) diff --git a/internal/controller/common/pods.go b/internal/controller/common/pods.go new file mode 100644 index 00000000..3965750e --- /dev/null +++ b/internal/controller/common/pods.go @@ -0,0 +1,17 @@ +package common + +import corev1 "k8s.io/api/core/v1" + +// PodReady reports whether a pod is Running with its Ready condition true, so a +// starting or CrashLoopBackOff pod (Running but not Ready) is not counted. +func PodReady(pod *corev1.Pod) bool { + if pod == nil || pod.Status.Phase != corev1.PodRunning { + return false + } + for _, c := range pod.Status.Conditions { + if c.Type == corev1.PodReady { + return c.Status == corev1.ConditionTrue + } + } + return false +} diff --git a/internal/controller/common/resource.go b/internal/controller/common/resource.go new file mode 100644 index 00000000..73f4c069 --- /dev/null +++ b/internal/controller/common/resource.go @@ -0,0 +1,222 @@ +package common + +import ( + "context" + "encoding/base64" + "encoding/json" + "maps" + "reflect" + + "github.com/wandb/operator/internal/logx" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// GetResource is a generic function that retrieves a Kubernetes resource. +// Returns (false, nil) if the resource is not found, or (true, nil) if found. +// Returns (false, error) for any other error. +func GetResource[T client.Object]( + ctx context.Context, + c client.Client, + namespacedName types.NamespacedName, + resourceTypeName string, + obj T, +) (bool, error) { + log := logx.GetSlog(ctx) + + err := c.Get(ctx, namespacedName, obj) + if err != nil { + if apierrors.IsNotFound(err) { + log.Debug( + "NotFound", "type", resourceTypeName, + "namespace", namespacedName.Namespace, "name", namespacedName.Name, + ) + return false, nil + } + log.Error( + "GetResourceError", logx.ErrAttr(err), "type", resourceTypeName, + "namespace", namespacedName.Namespace, "name", namespacedName.Name, + ) + return false, err + } + log.Debug( + "Found", "type", resourceTypeName, + "namespace", namespacedName.Namespace, "name", namespacedName.Name, + ) + return true, nil +} + +type CrudAction string + +const ( + NoAction = "" + CreateAction = "Create" + UpdateAction = "Update" + UnchangedAction = "Unchanged" + DeleteAction = "Delete" +) + +// CrudResource is a generic function that gets a resource, and creates it if not found, or updates it if it exists. +// The getter function should return (nil, nil) if the resource is not found. +func CrudResource[T client.Object](ctx context.Context, c client.Client, desired T, actual T) (CrudAction, error) { + log := logx.GetSlog(ctx) + + var err error + var action CrudAction + desiredExists := !IsNil(desired) && desired.GetName() != "" + actualExists := !IsNil(actual) && actual.GetName() != "" + + if actualExists && desiredExists { + if resourceManagedFieldsEqual(desired, actual) { + action = UnchangedAction + } else { + action = UpdateAction + prepareResourceUpdate(desired, actual) + err = c.Update(ctx, desired) + } + } + if !actualExists && desiredExists { + action = CreateAction + err = c.Create(ctx, desired) + } + if actualExists && !desiredExists { + action = DeleteAction + err = c.Delete(ctx, actual) + } + if action != NoAction && action != UnchangedAction { + if desiredExists { + log.Info(string(action), "namespace", desired.GetNamespace(), "name", desired.GetName()) + } else if actualExists { + log.Info(string(action), "namespace", actual.GetNamespace(), "name", actual.GetName()) + } + } + if err != nil { + log.Error("error on crud resource", logx.ErrAttr(err), "action", action) + } + return action, err +} + +func resourceManagedFieldsEqual(desired, actual client.Object) bool { + if !mapContains(actual.GetLabels(), desired.GetLabels()) || + !mapContains(actual.GetAnnotations(), desired.GetAnnotations()) || + !ownerReferencesContain(actual.GetOwnerReferences(), desired.GetOwnerReferences()) { + return false + } + + return resourceContentEqual(desired, actual) +} + +func resourceContentEqual(desired, actual client.Object) bool { + toContent := func(obj client.Object) (map[string]any, bool) { + data, err := json.Marshal(obj) + if err != nil { + return nil, false + } + content := map[string]any{} + if err := json.Unmarshal(data, &content); err != nil { + return nil, false + } + delete(content, "apiVersion") + delete(content, "kind") + delete(content, "metadata") + delete(content, "status") + normalizeSecretStringData(content) + return content, true + } + + desiredContent, desiredOK := toContent(desired) + actualContent, actualOK := toContent(actual) + return desiredOK && actualOK && reflect.DeepEqual(desiredContent, actualContent) +} + +func normalizeSecretStringData(content map[string]any) { + stringData, ok := content["stringData"].(map[string]any) + if !ok { + return + } + data, _ := content["data"].(map[string]any) + if data == nil { + data = map[string]any{} + } + for key, value := range stringData { + text, ok := value.(string) + if !ok { + continue + } + data[key] = base64.StdEncoding.EncodeToString([]byte(text)) + } + content["data"] = data + delete(content, "stringData") +} + +func mapContains(actual, desired map[string]string) bool { + for key, value := range desired { + if actual[key] != value { + return false + } + } + return true +} + +func ownerReferencesContain(actual, desired []metav1.OwnerReference) bool { + for _, desiredRef := range desired { + found := false + for _, actualRef := range actual { + if reflect.DeepEqual(actualRef, desiredRef) { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +func prepareResourceUpdate(desired, actual client.Object) { + desired.SetResourceVersion(actual.GetResourceVersion()) + desired.SetUID(actual.GetUID()) + desired.SetCreationTimestamp(actual.GetCreationTimestamp()) + desired.SetGeneration(actual.GetGeneration()) + desired.SetManagedFields(actual.GetManagedFields()) + desired.SetFinalizers(actual.GetFinalizers()) + desired.SetDeletionTimestamp(actual.GetDeletionTimestamp()) + + labels := maps.Clone(actual.GetLabels()) + if labels == nil { + labels = map[string]string{} + } + maps.Copy(labels, desired.GetLabels()) + desired.SetLabels(labels) + + annotations := maps.Clone(actual.GetAnnotations()) + if annotations == nil { + annotations = map[string]string{} + } + maps.Copy(annotations, desired.GetAnnotations()) + desired.SetAnnotations(annotations) + + ownerReferences := append([]metav1.OwnerReference(nil), actual.GetOwnerReferences()...) + for _, desiredRef := range desired.GetOwnerReferences() { + if !ownerReferencesContain(ownerReferences, []metav1.OwnerReference{desiredRef}) { + ownerReferences = append(ownerReferences, desiredRef) + } + } + desired.SetOwnerReferences(ownerReferences) +} + +// IsNil checks if the generic value v is a pointer and if that pointer is nil. +// It returns false if true is a non-pointer type, or if it's a non-nil pointer. +func IsNil[T any](v T) bool { + val := reflect.ValueOf(v) + + if val.Kind() != reflect.Pointer { + return false + } + + // Since we've already checked that the Kind is Pointer, we can safely call IsNil(). + return val.IsNil() +} diff --git a/internal/controller/common/resource_test.go b/internal/controller/common/resource_test.go new file mode 100644 index 00000000..4d3e4c2f --- /dev/null +++ b/internal/controller/common/resource_test.go @@ -0,0 +1,123 @@ +package common + +import ( + "context" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +type updateCountingClient struct { + client.Client + updates int +} + +func (c *updateCountingClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + c.updates++ + return c.Client.Update(ctx, obj, opts...) +} + +func TestCrudResourceSkipsUnchangedManagedFields(t *testing.T) { + t.Parallel() + + actual := testConfigMap("value") + actual.Labels["co-manager"] = "preserve" + actual.Finalizers = []string{"co-manager/finalizer"} + c := newUpdateCountingClient(t, actual) + desired := testConfigMap("value") + + action, err := CrudResource(context.Background(), c, desired, actual.DeepCopy()) + if err != nil { + t.Fatalf("reconcile resource: %v", err) + } + if action != UnchangedAction { + t.Fatalf("action = %q, want %q", action, UnchangedAction) + } + if c.updates != 0 { + t.Fatalf("updates = %d, want 0", c.updates) + } +} + +func TestCrudResourcePreservesCoManagedMetadataOnUpdate(t *testing.T) { + t.Parallel() + + actual := testConfigMap("old") + actual.Labels["co-manager"] = "preserve" + actual.Finalizers = []string{"co-manager/finalizer"} + c := newUpdateCountingClient(t, actual) + desired := testConfigMap("new") + + action, err := CrudResource(context.Background(), c, desired, actual.DeepCopy()) + if err != nil { + t.Fatalf("reconcile resource: %v", err) + } + if action != UpdateAction { + t.Fatalf("action = %q, want %q", action, UpdateAction) + } + if c.updates != 1 { + t.Fatalf("updates = %d, want 1", c.updates) + } + + updated := &corev1.ConfigMap{} + if err := c.Get(context.Background(), client.ObjectKeyFromObject(actual), updated); err != nil { + t.Fatalf("get updated ConfigMap: %v", err) + } + if updated.Data["key"] != "new" { + t.Fatalf("data = %q, want new", updated.Data["key"]) + } + if updated.Labels["co-manager"] != "preserve" { + t.Fatal("co-managed label was removed") + } + if len(updated.Finalizers) != 1 || updated.Finalizers[0] != "co-manager/finalizer" { + t.Fatalf("finalizers = %v, want co-manager finalizer", updated.Finalizers) + } +} + +func TestCrudResourceTreatsSecretStringDataAsExistingData(t *testing.T) { + t.Parallel() + + actual := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "connection", Namespace: "default"}, + Data: map[string][]byte{"Password": []byte("secret")}, + } + c := newUpdateCountingClient(t, actual) + desired := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "connection", Namespace: "default"}, + StringData: map[string]string{"Password": "secret"}, + } + + action, err := CrudResource(context.Background(), c, desired, actual.DeepCopy()) + if err != nil { + t.Fatalf("reconcile resource: %v", err) + } + if action != UnchangedAction { + t.Fatalf("action = %q, want %q", action, UnchangedAction) + } + if c.updates != 0 { + t.Fatalf("updates = %d, want 0", c.updates) + } +} + +func testConfigMap(value string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + Namespace: "default", + Labels: map[string]string{"managed-by": "wandb"}, + }, + Data: map[string]string{"key": value}, + } +} + +func newUpdateCountingClient(t *testing.T, objects ...client.Object) *updateCountingClient { + t.Helper() + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("add core API to scheme: %v", err) + } + return &updateCountingClient{Client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()} +} diff --git a/internal/controller/common/retention.go b/internal/controller/common/retention.go new file mode 100644 index 00000000..8ca2fc27 --- /dev/null +++ b/internal/controller/common/retention.go @@ -0,0 +1,35 @@ +package common + +import ( + apiv2 "github.com/wandb/operator/api/v2" + "k8s.io/apimachinery/pkg/labels" +) + +type OnDeletePolicy string + +const ( + Purge OnDeletePolicy = "purge" + Detach OnDeletePolicy = "detach" +) + +type OnDeleteRule struct { + Policy OnDeletePolicy + Selector labels.Selector +} + +// ToOnDeleteRule maps the user-facing RetentionPolicy onto an OnDeleteRule +// scoped to resources labelled with the given component name. +func ToOnDeleteRule( + wandb *apiv2.WeightsAndBiases, + retentionPolicy apiv2.RetentionPolicy, + componentName string, +) OnDeleteRule { + policy := Detach + if retentionPolicy.OnDelete == apiv2.PurgeOnDelete { + policy = Purge + } + return OnDeleteRule{ + Policy: policy, + Selector: labels.SelectorFromSet(BuildWandbLabels(wandb, componentName)), + } +} diff --git a/internal/controller/common/service.go b/internal/controller/common/service.go new file mode 100644 index 00000000..d23498fb --- /dev/null +++ b/internal/controller/common/service.go @@ -0,0 +1,19 @@ +package common + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// NormalizeServicePorts fills the port fields the API server defaults (protocol, +// targetPort) so specs built from manifests round-trip equal to what is stored. +func NormalizeServicePorts(ports []corev1.ServicePort) { + for i := range ports { + if ports[i].Protocol == "" { + ports[i].Protocol = corev1.ProtocolTCP + } + if ports[i].TargetPort == (intstr.IntOrString{}) { + ports[i].TargetPort = intstr.FromInt32(ports[i].Port) + } + } +} diff --git a/internal/controller/common/service_test.go b/internal/controller/common/service_test.go new file mode 100644 index 00000000..360473a5 --- /dev/null +++ b/internal/controller/common/service_test.go @@ -0,0 +1,25 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestNormalizeServicePorts(t *testing.T) { + ports := []corev1.ServicePort{ + {Name: "http", Port: 8080}, + {Name: "grpc", Port: 9000, Protocol: corev1.ProtocolUDP, TargetPort: intstr.FromString("grpc")}, + } + NormalizeServicePorts(ports) + + require.Equal(t, corev1.ProtocolTCP, ports[0].Protocol) + require.Equal(t, intstr.FromInt32(8080), ports[0].TargetPort) + + require.Equal(t, corev1.ProtocolUDP, ports[1].Protocol, "explicit protocol is kept") + require.Equal(t, intstr.FromString("grpc"), ports[1].TargetPort, "explicit targetPort is kept") + + NormalizeServicePorts(nil) +} diff --git a/internal/controller/common/state.go b/internal/controller/common/state.go new file mode 100644 index 00000000..36119590 --- /dev/null +++ b/internal/controller/common/state.go @@ -0,0 +1,14 @@ +package common + +const ( + ErrorState string = "Error" + PendingState string = "Pending" + HealthyState string = "Healthy" + DegradedState string = "Degraded" + UnknownState string = "Unknown" + UnavailableState string = "Unavailable" +) + +var NotReadyStates = []string{ + ErrorState, PendingState, UnavailableState, +} diff --git a/internal/controller/ctrlqueue/ctrl_state.go b/internal/controller/ctrlqueue/ctrl_state.go new file mode 100644 index 00000000..68f52f6e --- /dev/null +++ b/internal/controller/ctrlqueue/ctrl_state.go @@ -0,0 +1,63 @@ +package ctrlqueue + +import ( + "time" + + ctrl "sigs.k8s.io/controller-runtime" +) + +// CtrlScope defines the scope of the controller operation for determining +// that level of the reconciliation loop is complete. +// A larger numeric scope value indicates a broader scope. +type CtrlScope int + +const ( + NoScope CtrlScope = 0 // an `exitScope` of NoScope is simply to continue + PackageScope CtrlScope = 1 + ReconcilerScope CtrlScope = 2 +) + +type CtrlState interface { + ShouldExit(scope CtrlScope) bool + ReconcilerResult() (ctrl.Result, error) +} + +func CtrlError(err error) CtrlState { + return &ctrlStateImpl{ + exitScope: ReconcilerScope, + err: err, + result: ctrl.Result{}, + } +} + +func CtrlContinue() CtrlState { + return CtrlDone(NoScope) +} + +func CtrlDoneUntil(scope CtrlScope, requeueAfter time.Duration) CtrlState { + return &ctrlStateImpl{ + exitScope: scope, + result: ctrl.Result{RequeueAfter: requeueAfter}, + } +} + +func CtrlDone(scope CtrlScope) CtrlState { + return &ctrlStateImpl{ + exitScope: scope, + result: ctrl.Result{}, + } +} + +type ctrlStateImpl struct { + exitScope CtrlScope + err error + result ctrl.Result +} + +func (d *ctrlStateImpl) ShouldExit(scope CtrlScope) bool { + return d.exitScope >= scope +} + +func (d *ctrlStateImpl) ReconcilerResult() (ctrl.Result, error) { + return d.result, d.err +} diff --git a/internal/controller/ctrlqueue/ctrl_state_test.go b/internal/controller/ctrlqueue/ctrl_state_test.go new file mode 100644 index 00000000..20395c7c --- /dev/null +++ b/internal/controller/ctrlqueue/ctrl_state_test.go @@ -0,0 +1,271 @@ +package ctrlqueue + +import ( + "errors" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + ctrl "sigs.k8s.io/controller-runtime" +) + +var _ = Describe("CtrlState", func() { + Describe("CtrlError", func() { + It("should create a state with error and ReconcilerScope", func() { + err := errors.New("test error") + state := CtrlError(err) + + Expect(state).ToNot(BeNil()) + Expect(state.ShouldExit(NoScope)).To(BeTrue()) + Expect(state.ShouldExit(PackageScope)).To(BeTrue()) + Expect(state.ShouldExit(ReconcilerScope)).To(BeTrue()) + + result, returnedErr := state.ReconcilerResult() + Expect(returnedErr).To(Equal(err)) + Expect(result).To(Equal(ctrl.Result{})) + }) + + It("should handle nil error", func() { + state := CtrlError(nil) + + Expect(state).ToNot(BeNil()) + result, err := state.ReconcilerResult() + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{})) + }) + }) + + Describe("CtrlContinue", func() { + It("should create a state with NoScope that only exits at NoScope level", func() { + state := CtrlContinue() + + Expect(state).ToNot(BeNil()) + Expect(state.ShouldExit(NoScope)).To(BeTrue()) + Expect(state.ShouldExit(PackageScope)).To(BeFalse()) + Expect(state.ShouldExit(ReconcilerScope)).To(BeFalse()) + + result, err := state.ReconcilerResult() + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{})) + }) + }) + + Describe("CtrlDone", func() { + Context("when scope is NoScope", func() { + It("should exit at NoScope level only", func() { + state := CtrlDone(NoScope) + + Expect(state).ToNot(BeNil()) + Expect(state.ShouldExit(NoScope)).To(BeTrue()) + Expect(state.ShouldExit(PackageScope)).To(BeFalse()) + Expect(state.ShouldExit(ReconcilerScope)).To(BeFalse()) + + result, err := state.ReconcilerResult() + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{})) + }) + }) + + Context("when scope is PackageScope", func() { + It("should exit at PackageScope and higher", func() { + state := CtrlDone(PackageScope) + + Expect(state).ToNot(BeNil()) + Expect(state.ShouldExit(NoScope)).To(BeTrue()) + Expect(state.ShouldExit(PackageScope)).To(BeTrue()) + Expect(state.ShouldExit(ReconcilerScope)).To(BeFalse()) + + result, err := state.ReconcilerResult() + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{})) + }) + }) + + Context("when scope is ReconcilerScope", func() { + It("should exit at all scopes", func() { + state := CtrlDone(ReconcilerScope) + + Expect(state).ToNot(BeNil()) + Expect(state.ShouldExit(NoScope)).To(BeTrue()) + Expect(state.ShouldExit(PackageScope)).To(BeTrue()) + Expect(state.ShouldExit(ReconcilerScope)).To(BeTrue()) + + result, err := state.ReconcilerResult() + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{})) + }) + }) + }) + + Describe("CtrlDoneUntil", func() { + Context("when scope is NoScope", func() { + It("should exit at NoScope level only with requeue duration", func() { + duration := 5 * time.Minute + state := CtrlDoneUntil(NoScope, duration) + + Expect(state).ToNot(BeNil()) + Expect(state.ShouldExit(NoScope)).To(BeTrue()) + Expect(state.ShouldExit(PackageScope)).To(BeFalse()) + Expect(state.ShouldExit(ReconcilerScope)).To(BeFalse()) + + result, err := state.ReconcilerResult() + Expect(err).To(BeNil()) + Expect(result.RequeueAfter).To(Equal(duration)) + }) + }) + + Context("when scope is PackageScope", func() { + It("should exit at PackageScope and higher with requeue", func() { + duration := 10 * time.Second + state := CtrlDoneUntil(PackageScope, duration) + + Expect(state).ToNot(BeNil()) + Expect(state.ShouldExit(NoScope)).To(BeTrue()) + Expect(state.ShouldExit(PackageScope)).To(BeTrue()) + Expect(state.ShouldExit(ReconcilerScope)).To(BeFalse()) + + result, err := state.ReconcilerResult() + Expect(err).To(BeNil()) + Expect(result.RequeueAfter).To(Equal(duration)) + }) + }) + + Context("when scope is ReconcilerScope", func() { + It("should exit at all scopes with requeue", func() { + duration := 1 * time.Hour + state := CtrlDoneUntil(ReconcilerScope, duration) + + Expect(state).ToNot(BeNil()) + Expect(state.ShouldExit(NoScope)).To(BeTrue()) + Expect(state.ShouldExit(PackageScope)).To(BeTrue()) + Expect(state.ShouldExit(ReconcilerScope)).To(BeTrue()) + + result, err := state.ReconcilerResult() + Expect(err).To(BeNil()) + Expect(result.RequeueAfter).To(Equal(duration)) + }) + }) + + Context("when duration is zero", func() { + It("should handle zero duration", func() { + state := CtrlDoneUntil(PackageScope, 0) + + Expect(state).ToNot(BeNil()) + result, err := state.ReconcilerResult() + Expect(err).To(BeNil()) + Expect(result.RequeueAfter).To(Equal(time.Duration(0))) + }) + }) + + Context("when duration is negative", func() { + It("should handle negative duration", func() { + duration := -5 * time.Second + state := CtrlDoneUntil(ReconcilerScope, duration) + + Expect(state).ToNot(BeNil()) + result, err := state.ReconcilerResult() + Expect(err).To(BeNil()) + Expect(result.RequeueAfter).To(Equal(duration)) + }) + }) + }) + + Describe("ShouldExit", func() { + Context("scope comparison logic", func() { + It("should correctly compare NoScope exit state", func() { + state := CtrlDone(NoScope) + Expect(state.ShouldExit(NoScope)).To(BeTrue()) + Expect(state.ShouldExit(PackageScope)).To(BeFalse()) + Expect(state.ShouldExit(ReconcilerScope)).To(BeFalse()) + }) + + It("should correctly compare PackageScope exit state", func() { + state := CtrlDone(PackageScope) + Expect(state.ShouldExit(NoScope)).To(BeTrue()) + Expect(state.ShouldExit(PackageScope)).To(BeTrue()) + Expect(state.ShouldExit(ReconcilerScope)).To(BeFalse()) + }) + + It("should correctly compare ReconcilerScope exit state", func() { + state := CtrlDone(ReconcilerScope) + Expect(state.ShouldExit(NoScope)).To(BeTrue()) + Expect(state.ShouldExit(PackageScope)).To(BeTrue()) + Expect(state.ShouldExit(ReconcilerScope)).To(BeTrue()) + }) + }) + }) + + Describe("ReconcilerResult", func() { + It("should return empty result and nil error for CtrlContinue", func() { + state := CtrlContinue() + result, err := state.ReconcilerResult() + Expect(err).To(BeNil()) + Expect(result).To(Equal(ctrl.Result{})) + Expect(result.Requeue).To(BeFalse()) // nolint:SA1019 + Expect(result.RequeueAfter).To(Equal(time.Duration(0))) + }) + + It("should return result with requeue duration", func() { + duration := 30 * time.Second + state := CtrlDoneUntil(PackageScope, duration) + result, err := state.ReconcilerResult() + Expect(err).To(BeNil()) + Expect(result.RequeueAfter).To(Equal(duration)) + }) + + It("should return error when present", func() { + expectedErr := errors.New("reconciliation error") + state := CtrlError(expectedErr) + result, err := state.ReconcilerResult() + Expect(err).To(Equal(expectedErr)) + Expect(result).To(Equal(ctrl.Result{})) + }) + }) + + Describe("Scope constants", func() { + It("should have correct numeric ordering", func() { + Expect(int(NoScope)).To(Equal(0)) + Expect(int(PackageScope)).To(Equal(1)) + Expect(int(ReconcilerScope)).To(Equal(2)) + Expect(NoScope < PackageScope).To(BeTrue()) + Expect(PackageScope < ReconcilerScope).To(BeTrue()) + }) + }) + + Describe("Integration scenarios", func() { + Context("when handler needs to continue", func() { + It("should allow reconciler to continue processing", func() { + state := CtrlContinue() + Expect(state.ShouldExit(PackageScope)).To(BeFalse()) + }) + }) + + Context("when handler is done", func() { + It("should exit handler but allow reconciler to continue", func() { + state := CtrlDone(PackageScope) + Expect(state.ShouldExit(PackageScope)).To(BeTrue()) + Expect(state.ShouldExit(ReconcilerScope)).To(BeFalse()) + }) + }) + + Context("when reconciler encounters error", func() { + It("should exit all scopes", func() { + state := CtrlError(errors.New("fatal error")) + Expect(state.ShouldExit(PackageScope)).To(BeTrue()) + Expect(state.ShouldExit(ReconcilerScope)).To(BeTrue()) + }) + }) + + Context("when reconciler needs requeue", func() { + It("should exit all scopes with requeue duration", func() { + duration := 2 * time.Minute + state := CtrlDoneUntil(ReconcilerScope, duration) + + Expect(state.ShouldExit(ReconcilerScope)).To(BeTrue()) + result, err := state.ReconcilerResult() + Expect(err).To(BeNil()) + Expect(result.RequeueAfter).To(Equal(duration)) + }) + }) + }) +}) diff --git a/internal/controller/infra/external/clickhouse/clickhouse.go b/internal/controller/infra/external/clickhouse/clickhouse.go new file mode 100644 index 00000000..79e5b2bd --- /dev/null +++ b/internal/controller/infra/external/clickhouse/clickhouse.go @@ -0,0 +1,89 @@ +package clickhouse + +import ( + "context" + "fmt" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/infra/external" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ConnectionSecretName = "wandb-clickhouse-connection" + +func connectionSecretName(key string) string { + if key == "" || key == apiv2.DefaultInstanceName { + return ConnectionSecretName + } + return fmt.Sprintf("%s-%s", ConnectionSecretName, key) +} + +func WriteState( + ctx context.Context, + c client.Client, + wandb *apiv2.WeightsAndBiases, + key string, + spec *apiv2.ClickHouseConnection, +) []metav1.Condition { + logger := ctrl.LoggerFrom(ctx) + + fields := map[string]corev1.SecretKeySelector{ + "url": spec.URL, + "Host": spec.Host, + "HTTPPort": spec.HTTPPort, + "TCPPort": spec.TCPPort, + "User": spec.Username, + "Password": spec.Password, + "Database": spec.Database, + } + + data, err := external.ResolveFields(ctx, c, wandb.Namespace, fields) + if err != nil { + logger.Error(err, "failed to resolve external clickhouse fields") + return []metav1.Condition{{ + Type: "Reconciled", + Status: metav1.ConditionFalse, + Reason: "ApiError", + }} + } + + nsName := types.NamespacedName{Namespace: wandb.Namespace, Name: connectionSecretName(key)} + return external.WriteConnectionSecret(ctx, c, wandb, nsName, data) +} + +func ReadState( + ctx context.Context, + c client.Client, + wandb *apiv2.WeightsAndBiases, + key string, + newConditions []metav1.Condition, +) ([]metav1.Condition, *apiv2.ClickHouseConnection) { + nsName := types.NamespacedName{Namespace: wandb.Namespace, Name: connectionSecretName(key)} + _, conditions, found := external.ReadConnectionSecret(ctx, c, nsName, newConditions) + if !found { + return conditions, nil + } + + localRef := corev1.LocalObjectReference{Name: nsName.Name} + return conditions, &apiv2.ClickHouseConnection{ + URL: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "url", Optional: ptr.To(false)}, + Host: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Host", Optional: ptr.To(false)}, + HTTPPort: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "HTTPPort", Optional: ptr.To(false)}, + TCPPort: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "TCPPort", Optional: ptr.To(false)}, + Username: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "User", Optional: ptr.To(false)}, + Password: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Password", Optional: ptr.To(false)}, + Database: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Database", Optional: ptr.To(false)}, + } +} + +func DeleteConnectionSecret(ctx context.Context, c client.Client, wandb *apiv2.WeightsAndBiases, key string) error { + return external.DeleteConnectionSecret(ctx, c, types.NamespacedName{ + Namespace: wandb.Namespace, + Name: connectionSecretName(key), + }) +} diff --git a/internal/controller/infra/external/common.go b/internal/controller/infra/external/common.go new file mode 100644 index 00000000..280d0ad7 --- /dev/null +++ b/internal/controller/infra/external/common.go @@ -0,0 +1,173 @@ +package external + +import ( + "context" + "fmt" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func ResolveSecretKey(ctx context.Context, c client.Client, namespace string, sel corev1.SecretKeySelector) (string, error) { + if sel.Name == "" { + return "", nil + } + secret := &corev1.Secret{} + found, err := common.GetResource(ctx, c, types.NamespacedName{Namespace: namespace, Name: sel.Name}, "Secret", secret) + if err != nil { + return "", err + } + if !found { + return "", fmt.Errorf("secret %s/%s not found", namespace, sel.Name) + } + val, ok := secret.Data[sel.Key] + if !ok { + if sel.Optional != nil && *sel.Optional { + return "", nil + } + return "", fmt.Errorf("key %q not found in secret %s/%s", sel.Key, namespace, sel.Name) + } + return string(val), nil +} + +func BuildWandbOwnerRef(c client.Client, wandb *apiv2.WeightsAndBiases) (metav1.OwnerReference, error) { + gvk, err := c.GroupVersionKindFor(wandb) + if err != nil { + return metav1.OwnerReference{}, fmt.Errorf("could not get GVK for wandb owner: %w", err) + } + return metav1.OwnerReference{ + APIVersion: gvk.GroupVersion().String(), + Kind: gvk.Kind, + Name: wandb.GetName(), + UID: wandb.GetUID(), + Controller: ptr.To(false), + BlockOwnerDeletion: ptr.To(false), + }, nil +} + +func DeleteConnectionSecret(ctx context.Context, c client.Client, nsName types.NamespacedName) error { + secret := &corev1.Secret{} + found, err := common.GetResource(ctx, c, nsName, "Secret", secret) + if err != nil { + return err + } + if found { + return c.Delete(ctx, secret) + } + return nil +} + +func WriteConnectionSecret( + ctx context.Context, + c client.Client, + wandb *apiv2.WeightsAndBiases, + nsName types.NamespacedName, + data map[string]string, +) []metav1.Condition { + actual := &corev1.Secret{} + found, err := common.GetResource(ctx, c, nsName, "Secret", actual) + if err != nil { + return []metav1.Condition{{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }} + } + if !found { + actual = nil + } + + ownerRef, err := BuildWandbOwnerRef(c, wandb) + if err != nil { + return []metav1.Condition{{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ControllerErrorReason, + }} + } + + desired := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsName.Name, + Namespace: nsName.Namespace, + OwnerReferences: []metav1.OwnerReference{ownerRef}, + }, + Type: corev1.SecretTypeOpaque, + StringData: data, + } + + if _, err = common.CrudResource(ctx, c, desired, actual); err != nil { + return []metav1.Condition{{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }} + } + return nil +} + +func ResolveFields( + ctx context.Context, + c client.Client, + namespace string, + fields map[string]corev1.SecretKeySelector, +) (map[string]string, error) { + data := map[string]string{} + for key, sel := range fields { + val, err := ResolveSecretKey(ctx, c, namespace, sel) + if err != nil { + return nil, fmt.Errorf("field %q: %w", key, err) + } + if val != "" { + data[key] = val + } + } + return data, nil +} + +func InferExternalStatus( + oldConditions, newConditions []metav1.Condition, + generation int64, + hasConnection bool, +) (string, bool, []metav1.Condition) { + state := common.HealthyState + ready := true + if !hasConnection { + state = common.ErrorState + ready = false + } + + updatedConditions := common.ComputeConditionUpdates( + oldConditions, + newConditions, + generation, + common.DefaultConditionExpiry, + ) + return state, ready, updatedConditions +} + +func ReadConnectionSecret( + ctx context.Context, + c client.Client, + nsName types.NamespacedName, + newConditions []metav1.Condition, +) (*corev1.Secret, []metav1.Condition, bool) { + secret := &corev1.Secret{} + found, err := common.GetResource(ctx, c, nsName, "Secret", secret) + if err != nil { + return nil, append(newConditions, metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }), false + } + if !found { + return nil, newConditions, false + } + return secret, newConditions, true +} diff --git a/internal/controller/infra/external/mysql/mysql.go b/internal/controller/infra/external/mysql/mysql.go new file mode 100644 index 00000000..8ed76000 --- /dev/null +++ b/internal/controller/infra/external/mysql/mysql.go @@ -0,0 +1,127 @@ +package mysql + +import ( + "context" + "fmt" + "net/url" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/infra/external" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ConnectionSecretName = "wandb-mysql-connection" +const caCertPath = "/etc/ssl/certs/mysql_ca.pem" +const sslCertPath = "/etc/ssl/certs/mysql_ssl_cert.pem" +const sslKeyPath = "/etc/ssl/certs/mysql_ssl_key.pem" + +// connectionSecretName returns the connection secret name for an instance. The +// reserved default instance keeps the historical name for backward +// compatibility; other instances are suffixed with their key. +func connectionSecretName(key string) string { + if key == "" || key == apiv2.DefaultInstanceName { + return ConnectionSecretName + } + return fmt.Sprintf("%s-%s", ConnectionSecretName, key) +} + +func WriteState( + ctx context.Context, + c client.Client, + wandb *apiv2.WeightsAndBiases, + key string, + spec *apiv2.MysqlConnection, +) []metav1.Condition { + logger := ctrl.LoggerFrom(ctx) + + fields := map[string]corev1.SecretKeySelector{ + "Host": spec.Host, + "Port": spec.Port, + "Database": spec.Database, + "Username": spec.Username, + "Password": spec.Password, + "Tls": spec.Tls, + "SslCa": spec.SslCa, + "SslCert": spec.SslCert, + "SslKey": spec.SslKey, + } + + data, err := external.ResolveFields(ctx, c, wandb.Namespace, fields) + if err != nil { + logger.Error(err, "failed to resolve external mysql fields") + return []metav1.Condition{{ + Type: "Reconciled", + Status: metav1.ConditionFalse, + Reason: "ApiError", + }} + } + + dbUrl := url.URL{ + Scheme: "mysql", + Host: fmt.Sprintf("%s:%s", data["Host"], data["Port"]), + User: url.UserPassword(data["Username"], data["Password"]), + Path: data["Database"], + } + values := dbUrl.Query() + if tls, ok := data["Tls"]; ok { + values.Set("tls", tls) + } + if _, ok := data["SslCa"]; ok { + if values.Get("tls") == "" { + values.Set("tls", "custom") + } + values.Set("ssl-ca", caCertPath) + } + if _, ok := data["SslCert"]; ok { + values.Set("ssl-cert", sslCertPath) + } + if _, ok := data["SslKey"]; ok { + values.Set("ssl-key", sslKeyPath) + } + dbUrl.RawQuery = values.Encode() + + data["url"] = dbUrl.String() + + nsName := types.NamespacedName{Namespace: wandb.Namespace, Name: connectionSecretName(key)} + return external.WriteConnectionSecret(ctx, c, wandb, nsName, data) +} + +func ReadState( + ctx context.Context, + c client.Client, + wandb *apiv2.WeightsAndBiases, + key string, + newConditions []metav1.Condition, +) ([]metav1.Condition, *apiv2.MysqlConnection) { + nsName := types.NamespacedName{Namespace: wandb.Namespace, Name: connectionSecretName(key)} + _, conditions, found := external.ReadConnectionSecret(ctx, c, nsName, newConditions) + if !found { + return conditions, nil + } + + localRef := corev1.LocalObjectReference{Name: nsName.Name} + return conditions, &apiv2.MysqlConnection{ + URL: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "url", Optional: ptr.To(false)}, + Host: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Host", Optional: ptr.To(false)}, + Port: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Port", Optional: ptr.To(false)}, + Database: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Database", Optional: ptr.To(false)}, + Username: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Username", Optional: ptr.To(false)}, + Password: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Password", Optional: ptr.To(false)}, + Tls: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Tls", Optional: ptr.To(true)}, + SslCa: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "SslCa", Optional: ptr.To(true)}, + SslCert: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "SslCert", Optional: ptr.To(true)}, + SslKey: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "SslKey", Optional: ptr.To(true)}, + } +} + +func DeleteConnectionSecret(ctx context.Context, c client.Client, wandb *apiv2.WeightsAndBiases, key string) error { + return external.DeleteConnectionSecret(ctx, c, types.NamespacedName{ + Namespace: wandb.Namespace, + Name: connectionSecretName(key), + }) +} diff --git a/internal/controller/infra/external/mysql/mysql_test.go b/internal/controller/infra/external/mysql/mysql_test.go new file mode 100644 index 00000000..77a82965 --- /dev/null +++ b/internal/controller/infra/external/mysql/mysql_test.go @@ -0,0 +1,90 @@ +package mysql + +import ( + "context" + "net/url" + "testing" + + "github.com/stretchr/testify/require" + apiv2 "github.com/wandb/operator/api/v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const mysqlSourceSecretName = "external-mysql" + +func mysqlSel(key string) corev1.SecretKeySelector { + return corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: mysqlSourceSecretName}, + Key: key, + } +} + +func TestWriteStateAddsCustomTLSParamsWhenCACertPresent(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + require.NoError(t, apiv2.AddToScheme(scheme)) + + source := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: mysqlSourceSecretName, Namespace: "default"}, + Data: map[string][]byte{ + "Host": []byte("mysql.example.com"), + "Port": []byte("3306"), + "Database": []byte("wandb"), + "Username": []byte("wandb"), + "Password": []byte("secret"), + "SslCa": []byte("---ca---"), + "SslCert": []byte("---cert---"), + "SslKey": []byte("---key---"), + }, + } + wandb := &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{APIVersion: "apps.wandb.com/v2", Kind: "WeightsAndBiases"}, + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default"}, + Spec: apiv2.WeightsAndBiasesSpec{ + MySQL: map[string]apiv2.MySQLSpec{apiv2.DefaultInstanceName: { + ExternalMysql: &apiv2.MysqlConnection{ + Host: mysqlSel("Host"), + Port: mysqlSel("Port"), + Database: mysqlSel("Database"), + Username: mysqlSel("Username"), + Password: mysqlSel("Password"), + SslCa: mysqlSel("SslCa"), + SslCert: mysqlSel("SslCert"), + SslKey: mysqlSel("SslKey"), + }, + }}, + }, + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(wandb, source).Build() + + conditions := WriteState(context.Background(), client, wandb, apiv2.DefaultInstanceName, wandb.Spec.MySQL[apiv2.DefaultInstanceName].ExternalMysql) + require.Nil(t, conditions) + + written := &corev1.Secret{} + require.NoError(t, client.Get(context.Background(), types.NamespacedName{Name: ConnectionSecretName, Namespace: "default"}, written)) + data := mysqlConnectionData(written) + parsed, err := url.Parse(data["url"]) + require.NoError(t, err) + require.Equal(t, "mysql", parsed.Scheme) + require.Equal(t, "mysql.example.com:3306", parsed.Host) + require.Equal(t, "/wandb", parsed.Path) + require.Equal(t, "custom", parsed.Query().Get("tls")) + require.Equal(t, caCertPath, parsed.Query().Get("ssl-ca")) + require.Equal(t, sslCertPath, parsed.Query().Get("ssl-cert")) + require.Equal(t, sslKeyPath, parsed.Query().Get("ssl-key")) +} + +func mysqlConnectionData(secret *corev1.Secret) map[string]string { + out := map[string]string{} + for k, v := range secret.Data { + out[k] = string(v) + } + for k, v := range secret.StringData { + out[k] = v + } + return out +} diff --git a/internal/controller/infra/external/objectstore/connection.go b/internal/controller/infra/external/objectstore/connection.go new file mode 100644 index 00000000..4c8d4b9f --- /dev/null +++ b/internal/controller/infra/external/objectstore/connection.go @@ -0,0 +1,135 @@ +package objectstore + +import ( + "fmt" + "net/url" + "strconv" + "strings" + + apiv2 "github.com/wandb/operator/api/v2" +) + +// ConnInfo is the resolved object-store connection: the read-side counterpart to WriteState, decoded back from the connection secret. +type ConnInfo struct { + Provider apiv2.ObjectStoreProvider + // URI is the provider-native location, e.g. "s3://bucket", "gs://bucket/prefix", or "https://acct.blob.core.windows.net/container". + URI string + // Bucket is the bare bucket/container name. + Bucket string + // Endpoint overrides the S3 API endpoint for S3-compatible providers (SeaweedFS, MinIO); empty for AWS S3, GCS, and Azure. + Endpoint string + Region string + AccessKey string + SecretKey string + // ForcePathStyle is required by most non-AWS S3-compatible providers. + ForcePathStyle bool + TlsEnabled bool + Port string + Path string +} + +// HasStaticCredentials reports whether explicit keys were provided; when false, credentials come from ambient identity (IAM role / workload identity). +func (c ConnInfo) HasStaticCredentials() bool { + return c.AccessKey != "" && c.SecretKey != "" +} + +// ParseConnection decodes an object-store connection secret's canonical `url` (scheme->provider, userinfo->creds, path->bucket, query->tls/region/forcePathStyle), falling back to discrete keys. +func ParseConnection(data map[string][]byte) (ConnInfo, error) { + get := func(k string) string { return string(data[k]) } + + raw := get("url") + if raw == "" { + return ConnInfo{}, fmt.Errorf("object store connection secret missing url") + } + u, err := url.Parse(raw) + if err != nil { + return ConnInfo{}, fmt.Errorf("parse object store url %q: %w", raw, err) + } + + info := ConnInfo{} + if u.User != nil { + info.AccessKey = u.User.Username() + if pw, ok := u.User.Password(); ok { + info.SecretKey = pw + } + } + if info.AccessKey == "" { + info.AccessKey = get("AccessKey") + } + if info.SecretKey == "" { + info.SecretKey = get("SecretKey") + } + + q := u.Query() + info.Region = q.Get("region") + if info.Region == "" { + info.Region = get("Region") + } + + switch strings.ToLower(u.Scheme) { + case "s3", "cw": + info.Provider = apiv2.ObjectStoreProviderS3 + bucket := strings.TrimPrefix(u.Path, "/") + host := u.Host + if bucket == "" { + // No path: bucket is the host (s3://my-bucket) or opaque part (s3:my-bucket), with no endpoint override. + if u.Opaque != "" { + bucket = u.Opaque + } else { + bucket = host + } + host = "" + } + info.Bucket = bucket + info.URI = "s3://" + bucket + // A host alongside a bucket path means an S3-compatible endpoint (SeaweedFS, MinIO); AWS S3 has no endpoint override. + if host != "" { + endpointScheme := "http" + if tls, _ := strconv.ParseBool(q.Get("tls")); tls { + endpointScheme = "https" + } + info.Endpoint = fmt.Sprintf("%s://%s", endpointScheme, host) + } + if fps := q.Get("forcePathStyle"); fps != "" { + info.ForcePathStyle, _ = strconv.ParseBool(fps) + } else { + // Non-AWS S3-compatible endpoints generally require path-style. + info.ForcePathStyle = info.Endpoint != "" + } + case "gs", "gcs": + info.Provider = apiv2.ObjectStoreProviderGCS + info.Bucket = u.Host + info.URI = "gs://" + u.Host + u.Path + case "azure", "az": + // az://<account>/<container>/<prefix> + info.Provider = apiv2.ObjectStoreProviderAzure + account := u.Host + container, prefix := splitBucketPath(u.Path) + info.Bucket = container + info.URI = azureBlobURI(account, container, prefix) + case "http", "https": + if !strings.Contains(u.Host, "blob.core.windows.net") { + return ConnInfo{}, fmt.Errorf("unsupported object store url scheme %q", u.Scheme) + } + info.Provider = apiv2.ObjectStoreProviderAzure + container, _ := splitBucketPath(u.Path) + info.Bucket = container + // Pass the container URI through verbatim (sans credentials/query). + info.URI = (&url.URL{Scheme: u.Scheme, Host: u.Host, Path: u.Path}).String() + default: + return ConnInfo{}, fmt.Errorf("unsupported object store url scheme %q", u.Scheme) + } + + if info.Bucket == "" && info.Provider != "" { + return ConnInfo{}, fmt.Errorf("object store url %q has no bucket/container", raw) + } + return info, nil +} + +func azureBlobURI(account, container, prefix string) string { + uri := fmt.Sprintf("https://%s.blob.core.windows.net/%s", account, container) + if prefix != "" { + uri += "/" + prefix + } + return uri +} diff --git a/internal/controller/infra/external/objectstore/connection_test.go b/internal/controller/infra/external/objectstore/connection_test.go new file mode 100644 index 00000000..4ef3b3bc --- /dev/null +++ b/internal/controller/infra/external/objectstore/connection_test.go @@ -0,0 +1,108 @@ +package objectstore + +import ( + "testing" + + "github.com/stretchr/testify/require" + + apiv2 "github.com/wandb/operator/api/v2" +) + +func TestParseConnectionS3Compatible(t *testing.T) { + // Managed SeaweedFS shape: s3://ak:sk@host:port/bucket?tls=false plus a + // discrete Region key. + data := map[string][]byte{ + "url": []byte("s3://ak:sk@seaweedfs.wandb.svc.cluster.local:8333/wandb-bucket?tls=false"), + "Region": []byte("us-east-1"), + } + + info, err := ParseConnection(data) + require.NoError(t, err) + require.Equal(t, apiv2.ObjectStoreProviderS3, info.Provider) + require.Equal(t, "wandb-bucket", info.Bucket) + require.Equal(t, "s3://wandb-bucket", info.URI) + require.Equal(t, "http://seaweedfs.wandb.svc.cluster.local:8333", info.Endpoint) + require.Equal(t, "us-east-1", info.Region) + require.Equal(t, "ak", info.AccessKey) + require.Equal(t, "sk", info.SecretKey) + require.True(t, info.ForcePathStyle) +} + +func TestParseConnectionS3TLS(t *testing.T) { + data := map[string][]byte{ + "url": []byte("s3://ak:sk@minio.example.com:9000/bucket?tls=true"), + } + info, err := ParseConnection(data) + require.NoError(t, err) + require.Equal(t, "https://minio.example.com:9000", info.Endpoint) +} + +func TestParseConnectionAWS(t *testing.T) { + // AWS S3 with IAM role: no host, no credentials. + data := map[string][]byte{ + "url": []byte("s3://my-bucket"), + "Region": []byte("us-west-2"), + } + info, err := ParseConnection(data) + require.NoError(t, err) + require.Equal(t, apiv2.ObjectStoreProviderS3, info.Provider) + require.Equal(t, "my-bucket", info.Bucket) + require.Empty(t, info.Endpoint) + require.False(t, info.ForcePathStyle) + require.False(t, info.HasStaticCredentials()) +} + +func TestParseConnectionDiscreteCredFallback(t *testing.T) { + data := map[string][]byte{ + "url": []byte("s3://host:9000/bucket"), + "AccessKey": []byte("ak"), + "SecretKey": []byte("sk"), + } + info, err := ParseConnection(data) + require.NoError(t, err) + require.Equal(t, "ak", info.AccessKey) + require.Equal(t, "sk", info.SecretKey) +} + +func TestParseConnectionGCS(t *testing.T) { + data := map[string][]byte{ + "url": []byte("gs://wandb-bucket/some/prefix"), + } + info, err := ParseConnection(data) + require.NoError(t, err) + require.Equal(t, apiv2.ObjectStoreProviderGCS, info.Provider) + require.Equal(t, "wandb-bucket", info.Bucket) + require.Equal(t, "gs://wandb-bucket/some/prefix", info.URI) +} + +func TestParseConnectionAzureHTTPS(t *testing.T) { + data := map[string][]byte{ + "url": []byte("https://acct.blob.core.windows.net/container/prefix"), + } + info, err := ParseConnection(data) + require.NoError(t, err) + require.Equal(t, apiv2.ObjectStoreProviderAzure, info.Provider) + require.Equal(t, "container", info.Bucket) + require.Equal(t, "https://acct.blob.core.windows.net/container/prefix", info.URI) +} + +func TestParseConnectionAzureScheme(t *testing.T) { + data := map[string][]byte{ + "url": []byte("az://acct:key@acct/container/prefix"), + } + info, err := ParseConnection(data) + require.NoError(t, err) + require.Equal(t, apiv2.ObjectStoreProviderAzure, info.Provider) + require.Equal(t, "container", info.Bucket) + require.Equal(t, "https://acct.blob.core.windows.net/container/prefix", info.URI) + require.Equal(t, "acct", info.AccessKey) + require.Equal(t, "key", info.SecretKey) +} + +func TestParseConnectionErrors(t *testing.T) { + _, err := ParseConnection(map[string][]byte{}) + require.Error(t, err) + + _, err = ParseConnection(map[string][]byte{"url": []byte("ftp://nope/bucket")}) + require.Error(t, err) +} diff --git a/internal/controller/infra/external/objectstore/objectstore.go b/internal/controller/infra/external/objectstore/objectstore.go new file mode 100644 index 00000000..1084957d --- /dev/null +++ b/internal/controller/infra/external/objectstore/objectstore.go @@ -0,0 +1,198 @@ +package objectstore + +import ( + "context" + "fmt" + "net/url" + "strconv" + "strings" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/infra/external" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ConnectionSecretName = "wandb-objectstore-connection" + +func connectionSecretName(key string) string { + if key == "" || key == apiv2.DefaultInstanceName { + return ConnectionSecretName + } + return fmt.Sprintf("%s-%s", ConnectionSecretName, key) +} + +func WriteState( + ctx context.Context, + c client.Client, + wandb *apiv2.WeightsAndBiases, + key string, + spec *apiv2.ObjectStoreConnection, +) ([]metav1.Condition, *apiv2.ObjectStoreConnection) { + logger := ctrl.LoggerFrom(ctx) + + fields := map[string]corev1.SecretKeySelector{ + "Host": spec.Endpoint, + "Port": spec.Port, + "AccessKey": spec.AccessKey, + "SecretKey": spec.SecretKey, + "Bucket": spec.Bucket, + "Path": spec.Path, + "Region": spec.Region, + "Provider": spec.Provider, + "TlsEnabled": spec.TlsEnabled, + "ForcePathStyle": spec.ForcePathStyle, + } + + data, err := external.ResolveFields(ctx, c, wandb.Namespace, fields) + if err != nil { + logger.Error(err, "failed to resolve external object store fields") + return []metav1.Condition{{ + Type: "Reconciled", + Status: metav1.ConditionFalse, + Reason: "ApiError", + }}, nil + } + + // Normalize the prefix once so every consumer joins it without stray slashes. + if trimmed := strings.Trim(data["Path"], "/"); trimmed != "" { + data["Path"] = trimmed + } else { + delete(data, "Path") + } + + provider := apiv2.ObjectStoreProvider(data["Provider"]) + if provider == "" { + provider = apiv2.ObjectStoreProviderS3 + } + data["Provider"] = string(provider) + + switch provider { + case apiv2.ObjectStoreProviderGCS: + data["url"] = buildGCSURL(data) + case apiv2.ObjectStoreProviderAzure: + data["url"] = buildAzureURL(data) + default: + // Consumers (Bufstream) render this verbatim, so derive it when the CR doesn't say. + if _, ok := data["ForcePathStyle"]; !ok { + data["ForcePathStyle"] = strconv.FormatBool(RequiresPathStyle(data["Host"])) + } + data["url"] = buildS3URL(data) + } + + nsName := types.NamespacedName{Namespace: wandb.Namespace, Name: connectionSecretName(key)} + if conditions := external.WriteConnectionSecret(ctx, c, wandb, nsName, data); conditions != nil { + return conditions, nil + } + + localRef := corev1.LocalObjectReference{Name: nsName.Name} + // ResolveFields only writes non-empty values, so any field that is + // legitimately absent for some deployment must be optional: Host (plain + // AWS S3 with no custom endpoint), AccessKey/SecretKey (IAM-role / + // workload-identity auth), Region (MinIO or region supplied out-of-band). + // url and Bucket are always written. + return nil, &apiv2.ObjectStoreConnection{ + Provider: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Provider", Optional: ptr.To(false)}, + URL: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "url", Optional: ptr.To(false)}, + Endpoint: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Host", Optional: ptr.To(true)}, + Port: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Port", Optional: ptr.To(true)}, + AccessKey: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "AccessKey", Optional: ptr.To(true)}, + SecretKey: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "SecretKey", Optional: ptr.To(true)}, + Bucket: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Bucket", Optional: ptr.To(false)}, + Path: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Path", Optional: ptr.To(true)}, + Region: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Region", Optional: ptr.To(true)}, + TlsEnabled: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "TlsEnabled", Optional: ptr.To(true)}, + ForcePathStyle: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "ForcePathStyle", Optional: ptr.To(true)}, + } +} + +// buildS3URL assembles s3://[accessKey:secretKey@][host[:port]]/bucket[/path]; host and creds are omitted for native AWS S3 / IAM-role auth. +func buildS3URL(data map[string]string) string { + bucketURL := url.URL{ + Scheme: "s3", + Path: joinBucketPrefix(data["Bucket"], data["Path"]), + } + if _, ok := data["Host"]; ok { + if _, ok := data["Port"]; ok { + bucketURL.Host = fmt.Sprintf("%s:%s", data["Host"], data["Port"]) + } else { + bucketURL.Host = data["Host"] + } + } + if _, ok := data["AccessKey"]; ok { + bucketURL.User = url.UserPassword(data["AccessKey"], data["SecretKey"]) + } + return bucketURL.String() +} + +// buildGCSURL assembles gs://<bucket>[/path]; creds default to workload identity, or accessKey (SA email) + secretKey (PEM key) as userinfo. +func buildGCSURL(data map[string]string) string { + bucket, path := splitBucketPath(data["Bucket"]) + path = joinBucketPrefix(path, data["Path"]) + bucketURL := url.URL{Scheme: "gs", Host: bucket} + if path != "" { + bucketURL.Path = "/" + path + } + if ak := data["AccessKey"]; ak != "" { + bucketURL.User = url.UserPassword(ak, data["SecretKey"]) + } + return bucketURL.String() +} + +// buildAzureURL assembles az://<account>/<container>[/path] from accessKey (account), bucket (container), and secretKey (account key, when set). +func buildAzureURL(data map[string]string) string { + account := data["AccessKey"] + container, path := splitBucketPath(data["Bucket"]) + path = joinBucketPrefix(path, data["Path"]) + bucketURL := url.URL{Scheme: "az", Host: account, Path: "/" + container} + if path != "" { + bucketURL.Path += "/" + path + } + if key := data["SecretKey"]; key != "" { + bucketURL.User = url.UserPassword("", key) + } + return bucketURL.String() +} + +// joinBucketPrefix appends a normalized key prefix to base (a bucket or an existing prefix). +func joinBucketPrefix(base, prefix string) string { + prefix = strings.Trim(prefix, "/") + switch { + case prefix == "": + return base + case base == "": + return prefix + default: + return base + "/" + prefix + } +} + +// splitBucketPath splits "bucket/optional/prefix" into the leading bucket (or container) segment and the remaining object prefix. +func splitBucketPath(raw string) (bucket, path string) { + trimmed := strings.TrimPrefix(raw, "/") + if slash := strings.IndexByte(trimmed, '/'); slash >= 0 { + return trimmed[:slash], trimmed[slash+1:] + } + return trimmed, "" +} + +func ReadState( + _ context.Context, + _ client.Client, + _ *apiv2.WeightsAndBiases, + _ string, + newConditions []metav1.Condition, +) []metav1.Condition { + return newConditions +} + +func DeleteConnectionSecret(ctx context.Context, c client.Client, wandb *apiv2.WeightsAndBiases, key string) error { + return external.DeleteConnectionSecret(ctx, c, types.NamespacedName{ + Namespace: wandb.Namespace, + Name: connectionSecretName(key), + }) +} diff --git a/internal/controller/infra/external/objectstore/objectstore_test.go b/internal/controller/infra/external/objectstore/objectstore_test.go new file mode 100644 index 00000000..84630d75 --- /dev/null +++ b/internal/controller/infra/external/objectstore/objectstore_test.go @@ -0,0 +1,342 @@ +package objectstore + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + apiv2 "github.com/wandb/operator/api/v2" +) + +const sourceSecretName = "ext-objectstore" + +func sel(key string) corev1.SecretKeySelector { + return corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: sourceSecretName}, + Key: key, + } +} + +// writeStateFixture builds a fake client seeded with a source Secret holding +// the given keys, plus a wandb whose externalObjectStore selectors point at +// the keys named in present. +func writeStateFixture(t *testing.T, sourceData map[string]string, present map[string]bool) (*apiv2.WeightsAndBiases, *corev1.Secret, []metav1.Condition, *apiv2.ObjectStoreConnection) { + t.Helper() + return writeStateFixtureProvider(t, "", sourceData, present) +} + +func writeStateFixtureProvider(t *testing.T, provider apiv2.ObjectStoreProvider, sourceData map[string]string, present map[string]bool) (*apiv2.WeightsAndBiases, *corev1.Secret, []metav1.Condition, *apiv2.ObjectStoreConnection) { + t.Helper() + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + require.NoError(t, apiv2.AddToScheme(scheme)) + + data := map[string][]byte{} + for k, v := range sourceData { + data[k] = []byte(v) + } + source := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: sourceSecretName, Namespace: "default"}, + Data: data, + } + + ext := &apiv2.ObjectStoreConnection{} + if provider != "" { + source.Data["Provider"] = []byte(string(provider)) + ext.Provider = sel("Provider") + } + if present["Host"] { + ext.Endpoint = sel("Host") + } + if present["Port"] { + ext.Port = sel("Port") + } + if present["AccessKey"] { + ext.AccessKey = sel("AccessKey") + } + if present["SecretKey"] { + ext.SecretKey = sel("SecretKey") + } + if present["Bucket"] { + ext.Bucket = sel("Bucket") + } + if present["Path"] { + ext.Path = sel("Path") + } + if present["Region"] { + ext.Region = sel("Region") + } + if present["TlsEnabled"] { + ext.TlsEnabled = sel("TlsEnabled") + } + if present["ForcePathStyle"] { + ext.ForcePathStyle = sel("ForcePathStyle") + } + + wandb := &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{APIVersion: "apps.wandb.com/v2", Kind: "WeightsAndBiases"}, + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default"}, + Spec: apiv2.WeightsAndBiasesSpec{ + ObjectStore: map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: {ExternalObjectStore: ext}, + }, + }, + } + + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(wandb, source).Build() + conditions, conn := WriteState(context.Background(), c, wandb, apiv2.DefaultInstanceName, ext) + + written := &corev1.Secret{} + require.NoError(t, c.Get(context.Background(), types.NamespacedName{Name: ConnectionSecretName, Namespace: "default"}, written)) + return wandb, written, conditions, conn +} + +// connectionData merges Data and StringData since WriteConnectionSecret writes +// via StringData and the fake client does not run the apiserver's +// StringData->Data normalization. +func connectionData(s *corev1.Secret) map[string]string { + out := map[string]string{} + for k, v := range s.Data { + out[k] = string(v) + } + for k, v := range s.StringData { + out[k] = v + } + return out +} + +func TestWriteState_MinioNoRegion(t *testing.T) { + _, written, conditions, conn := writeStateFixture(t, + map[string]string{ + "Host": "minio.local", + "Port": "9000", + "AccessKey": "minio", + "SecretKey": "minio123", + "Bucket": "my-bucket", + }, + map[string]bool{"Host": true, "Port": true, "AccessKey": true, "SecretKey": true, "Bucket": true}, + ) + require.Nil(t, conditions, "expected success") + require.NotNil(t, conn) + + data := connectionData(written) + require.Equal(t, "s3://minio:minio123@minio.local:9000/my-bucket", data["url"]) + require.NotContains(t, data, "Region") +} + +func TestWriteState_AwsIamNoHostNoKeys(t *testing.T) { + _, written, conditions, conn := writeStateFixture(t, + map[string]string{ + "Region": "us-east-1", + "Bucket": "my-bucket", + }, + map[string]bool{"Region": true, "Bucket": true}, + ) + require.Nil(t, conditions, "region-only config must not fail") + require.NotNil(t, conn) + + data := connectionData(written) + require.Equal(t, "s3://my-bucket", data["url"], "no host means no authority, no creds") + require.Equal(t, "us-east-1", data["Region"]) + require.NotContains(t, data, "Host") + require.NotContains(t, data, "AccessKey") + require.NotContains(t, data, "SecretKey") +} + +func TestWriteState_HostNoPortNoKeys(t *testing.T) { + _, written, conditions, _ := writeStateFixture(t, + map[string]string{ + "Host": "minio.local", + "Bucket": "my-bucket", + }, + map[string]bool{"Host": true, "Bucket": true}, + ) + require.Nil(t, conditions) + + data := connectionData(written) + require.Equal(t, "s3://minio.local/my-bucket", data["url"], "host without port or creds") +} + +func TestWriteState_FullConfig(t *testing.T) { + _, written, conditions, conn := writeStateFixture(t, + map[string]string{ + "Host": "s3.example.com", + "Port": "443", + "AccessKey": "access", + "SecretKey": "secret", + "Bucket": "my-bucket", + "Region": "us-west-2", + }, + map[string]bool{"Host": true, "Port": true, "AccessKey": true, "SecretKey": true, "Bucket": true, "Region": true}, + ) + require.Nil(t, conditions) + require.NotNil(t, conn) + + data := connectionData(written) + require.Equal(t, "s3://access:secret@s3.example.com:443/my-bucket", data["url"]) + require.Equal(t, "us-west-2", data["Region"]) + + // Optional flags: only url and Bucket are required. + require.NotNil(t, conn.URL.Optional) + require.False(t, *conn.URL.Optional) + require.NotNil(t, conn.Bucket.Optional) + require.False(t, *conn.Bucket.Optional) + for _, s := range []corev1.SecretKeySelector{conn.Endpoint, conn.Port, conn.AccessKey, conn.SecretKey, conn.Region} { + require.NotNil(t, s.Optional) + require.True(t, *s.Optional) + } +} + +func TestWriteState_PathPrefix(t *testing.T) { + // S3-compatible endpoint with a key prefix. + _, written, conditions, conn := writeStateFixture(t, + map[string]string{ + "Host": "minio.local", + "Port": "9000", + "AccessKey": "minio", + "SecretKey": "minio123", + "Bucket": "my-bucket", + "Path": "team/prefix", + }, + map[string]bool{"Host": true, "Port": true, "AccessKey": true, "SecretKey": true, "Bucket": true, "Path": true}, + ) + require.Nil(t, conditions) + data := connectionData(written) + require.Equal(t, "s3://minio:minio123@minio.local:9000/my-bucket/team/prefix", data["url"]) + require.Equal(t, "team/prefix", data["Path"]) + require.Equal(t, "Path", conn.Path.Key) + require.NotNil(t, conn.Path.Optional) + require.True(t, *conn.Path.Optional) + + // Native AWS with a prefix; slashes are normalized. + _, written, conditions, _ = writeStateFixture(t, + map[string]string{"Bucket": "my-bucket", "Path": "/prefix/"}, + map[string]bool{"Bucket": true, "Path": true}, + ) + require.Nil(t, conditions) + data = connectionData(written) + require.Equal(t, "s3://my-bucket/prefix", data["url"]) + require.Equal(t, "prefix", data["Path"], "the stored key is normalized, not raw") +} + +func TestWriteState_GCSPathPrefix(t *testing.T) { + _, written, conditions, _ := writeStateFixtureProvider(t, apiv2.ObjectStoreProviderGCS, + map[string]string{"Bucket": "my-gcs-bucket", "Path": "team/prefix"}, + map[string]bool{"Bucket": true, "Path": true}, + ) + require.Nil(t, conditions) + require.Equal(t, "gs://my-gcs-bucket/team/prefix", connectionData(written)["url"]) +} + +func TestWriteState_ForcePathStyleDerived(t *testing.T) { + // Custom endpoint → path-style. + _, written, conditions, _ := writeStateFixture(t, + map[string]string{"Host": "minio.local", "Bucket": "b"}, + map[string]bool{"Host": true, "Bucket": true}, + ) + require.Nil(t, conditions) + require.Equal(t, "true", connectionData(written)["ForcePathStyle"]) + + // No endpoint (native AWS) → virtual-hosted. + _, written, conditions, _ = writeStateFixture(t, + map[string]string{"Bucket": "b"}, + map[string]bool{"Bucket": true}, + ) + require.Nil(t, conditions) + require.Equal(t, "false", connectionData(written)["ForcePathStyle"]) + + // An explicit AWS endpoint override is still a custom endpoint → path-style. + _, written, conditions, _ = writeStateFixture(t, + map[string]string{"Host": "s3.us-east-1.amazonaws.com", "Bucket": "b"}, + map[string]bool{"Host": true, "Bucket": true}, + ) + require.Nil(t, conditions) + require.Equal(t, "true", connectionData(written)["ForcePathStyle"]) + + // CoreWeave object storage is virtual-hosted. + _, written, conditions, _ = writeStateFixture(t, + map[string]string{"Host": "cwobject.com", "Bucket": "b"}, + map[string]bool{"Host": true, "Bucket": true}, + ) + require.Nil(t, conditions) + require.Equal(t, "false", connectionData(written)["ForcePathStyle"]) +} + +func TestWriteState_ForcePathStyleExplicitWins(t *testing.T) { + _, written, conditions, _ := writeStateFixture(t, + map[string]string{"Host": "minio.local", "Bucket": "b", "ForcePathStyle": "false"}, + map[string]bool{"Host": true, "Bucket": true, "ForcePathStyle": true}, + ) + require.Nil(t, conditions) + require.Equal(t, "false", connectionData(written)["ForcePathStyle"], "explicit CR value must not be overridden by derivation") +} + +func TestWriteState_GCSWorkloadIdentity(t *testing.T) { + _, written, conditions, conn := writeStateFixtureProvider(t, apiv2.ObjectStoreProviderGCS, + map[string]string{"Bucket": "my-gcs-bucket"}, + map[string]bool{"Bucket": true}, + ) + require.Nil(t, conditions) + require.NotNil(t, conn) + require.Equal(t, ConnectionSecretName, conn.Provider.Name) + require.Equal(t, "Provider", conn.Provider.Key) + + data := connectionData(written) + require.Equal(t, "gs://my-gcs-bucket", data["url"], "workload identity carries no credentials") + require.Equal(t, "gcs", data["Provider"]) + require.NotContains(t, data, "ForcePathStyle", "path-style is an S3-only concept") +} + +func TestWriteState_GCSWithPrefixAndKey(t *testing.T) { + _, written, conditions, _ := writeStateFixtureProvider(t, apiv2.ObjectStoreProviderGCS, + map[string]string{ + "Bucket": "my-gcs-bucket/sub/prefix", + "AccessKey": "sa@project.iam.gserviceaccount.com", + "SecretKey": "pemkey", + }, + map[string]bool{"Bucket": true, "AccessKey": true, "SecretKey": true}, + ) + require.Nil(t, conditions) + + data := connectionData(written) + require.Equal(t, "gs://sa%40project.iam.gserviceaccount.com:pemkey@my-gcs-bucket/sub/prefix", data["url"]) +} + +func TestWriteState_AzureWithKey(t *testing.T) { + _, written, conditions, conn := writeStateFixtureProvider(t, apiv2.ObjectStoreProviderAzure, + map[string]string{ + "AccessKey": "mystorageaccount", + "SecretKey": "accountkey==", + "Bucket": "mycontainer", + }, + map[string]bool{"AccessKey": true, "SecretKey": true, "Bucket": true}, + ) + require.Nil(t, conditions) + require.NotNil(t, conn) + require.Equal(t, ConnectionSecretName, conn.Provider.Name) + require.Equal(t, "Provider", conn.Provider.Key) + + data := connectionData(written) + require.Equal(t, "az://:accountkey==@mystorageaccount/mycontainer", data["url"]) + require.Equal(t, "azure", data["Provider"]) +} + +func TestWriteState_AzureAmbientIdentityWithPrefix(t *testing.T) { + _, written, conditions, _ := writeStateFixtureProvider(t, apiv2.ObjectStoreProviderAzure, + map[string]string{ + "AccessKey": "mystorageaccount", + "Bucket": "mycontainer/team/prefix", + }, + map[string]bool{"AccessKey": true, "Bucket": true}, + ) + require.Nil(t, conditions) + + data := connectionData(written) + require.Equal(t, "az://mystorageaccount/mycontainer/team/prefix", data["url"], "no key means ambient identity / AZURE_STORAGE_KEY") +} diff --git a/internal/controller/infra/external/objectstore/pathstyle.go b/internal/controller/infra/external/objectstore/pathstyle.go new file mode 100644 index 00000000..e69b1eb9 --- /dev/null +++ b/internal/controller/infra/external/objectstore/pathstyle.go @@ -0,0 +1,31 @@ +package objectstore + +import ( + "net" + "strings" +) + +// coreweaveDomains identify CoreWeave AI Object Storage endpoints, which are +// virtual-hosted (matching the server's cw:// handling). +var coreweaveDomains = []string{"cwobject.com", "cwlota.com", "coreweave.com"} + +// RequiresPathStyle reports whether an S3 endpoint needs path-style addressing: +// true for any custom endpoint except CoreWeave's virtual-hosted object storage. +func RequiresPathStyle(endpoint string) bool { + host := strings.ToLower(endpoint) + if i := strings.Index(host, "://"); i >= 0 { + host = host[i+3:] + } + if h, _, err := net.SplitHostPort(host); err == nil { + host = h + } + if host == "" { + return false + } + for _, domain := range coreweaveDomains { + if host == domain || strings.HasSuffix(host, "."+domain) { + return false + } + } + return true +} diff --git a/internal/controller/infra/external/objectstore/pathstyle_test.go b/internal/controller/infra/external/objectstore/pathstyle_test.go new file mode 100644 index 00000000..1e5bb516 --- /dev/null +++ b/internal/controller/infra/external/objectstore/pathstyle_test.go @@ -0,0 +1,40 @@ +package objectstore + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRequiresPathStyle(t *testing.T) { + cases := []struct { + endpoint string + want bool + }{ + {"", false}, + // Any explicit endpoint is path-style, AWS's own included: path-style + // works against AWS, and VPC endpoints require it. + {"s3.us-east-1.amazonaws.com", true}, + {"bucket.vpce-0abc.s3.us-west-2.vpce.amazonaws.com", true}, + {"minio.wandb.localhost", true}, + {"minio.wandb.localhost:8080", true}, + {"minio", true}, + {"minio:9000", true}, + {"seaweedfs.seaweedfs.svc.cluster.local:8333", true}, + {"http://minio.local:9000", true}, + {"https://s3.example.com", true}, + // CoreWeave object storage is virtual-hosted. + {"cwobject.com", false}, + {"accel-object.ord1.coreweave.com", false}, + {"foo.cwlota.com", false}, + {"COREWEAVE.COM", false}, + // Suffix match must anchor on a label boundary. + {"cwobject.com.evil.example", true}, + {"evil-cwobject.com", true}, + } + for _, tc := range cases { + t.Run(tc.endpoint, func(t *testing.T) { + require.Equal(t, tc.want, RequiresPathStyle(tc.endpoint)) + }) + } +} diff --git a/internal/controller/infra/external/redis/redis.go b/internal/controller/infra/external/redis/redis.go new file mode 100644 index 00000000..2bb07762 --- /dev/null +++ b/internal/controller/infra/external/redis/redis.go @@ -0,0 +1,113 @@ +package redis + +import ( + "context" + "fmt" + "net/url" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/infra/external" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ConnectionSecretName = "wandb-redis-connection" +const caCertPath = "/etc/ssl/certs/redis_ca.pem" + +func connectionSecretName(key string) string { + if key == "" || key == apiv2.DefaultInstanceName { + return ConnectionSecretName + } + return fmt.Sprintf("%s-%s", ConnectionSecretName, key) +} + +func WriteState( + ctx context.Context, + c client.Client, + wandb *apiv2.WeightsAndBiases, + key string, + spec *apiv2.RedisConnection, +) []metav1.Condition { + logger := ctrl.LoggerFrom(ctx) + + fields := map[string]corev1.SecretKeySelector{ + "Host": spec.Host, + "Port": spec.Port, + "Password": spec.Password, + "Tls": spec.Tls, + "SslCa": spec.SslCa, + } + + data, err := external.ResolveFields(ctx, c, wandb.Namespace, fields) + if err != nil { + logger.Error(err, "failed to resolve external redis fields") + return []metav1.Condition{{ + Type: "Reconciled", + Status: metav1.ConditionFalse, + Reason: "ApiError", + }} + } + + redisUrl := url.URL{ + Scheme: "redis", + Host: fmt.Sprintf("%s:%s", data["Host"], data["Port"]), + } + + if _, ok := data["Password"]; ok { + redisUrl.User = url.UserPassword(data["Password"], "") + } + + if _, ok := data["Tls"]; ok { + values := redisUrl.Query() + values.Add("tls", data["Tls"]) + redisUrl.RawQuery = values.Encode() + } + if _, ok := data["SslCa"]; ok { + values := redisUrl.Query() + if values.Get("tls") == "" { + values.Set("tls", "true") + } + values.Set("caCertPath", caCertPath) + redisUrl.RawQuery = values.Encode() + } + + data["url"] = redisUrl.String() + + nsName := types.NamespacedName{Namespace: wandb.Namespace, Name: connectionSecretName(key)} + return external.WriteConnectionSecret(ctx, c, wandb, nsName, data) +} + +func ReadState( + ctx context.Context, + c client.Client, + wandb *apiv2.WeightsAndBiases, + key string, + newConditions []metav1.Condition, +) ([]metav1.Condition, *apiv2.RedisConnection) { + nsName := types.NamespacedName{Namespace: wandb.Namespace, Name: connectionSecretName(key)} + _, conditions, found := external.ReadConnectionSecret(ctx, c, nsName, newConditions) + if !found { + return conditions, nil + } + + localRef := corev1.LocalObjectReference{Name: nsName.Name} + return conditions, &apiv2.RedisConnection{ + URL: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "url", Optional: ptr.To(false)}, + Host: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Host", Optional: ptr.To(false)}, + Port: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Port", Optional: ptr.To(false)}, + Password: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Password", Optional: ptr.To(true)}, + Tls: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Tls", Optional: ptr.To(true)}, + SslCa: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "SslCa", Optional: ptr.To(true)}, + } +} + +func DeleteConnectionSecret(ctx context.Context, c client.Client, wandb *apiv2.WeightsAndBiases, key string) error { + return external.DeleteConnectionSecret(ctx, c, types.NamespacedName{ + Namespace: wandb.Namespace, + Name: connectionSecretName(key), + }) +} diff --git a/internal/controller/infra/external/redis/redis_test.go b/internal/controller/infra/external/redis/redis_test.go new file mode 100644 index 00000000..ba789e7a --- /dev/null +++ b/internal/controller/infra/external/redis/redis_test.go @@ -0,0 +1,79 @@ +package redis + +import ( + "context" + "net/url" + "testing" + + "github.com/stretchr/testify/require" + apiv2 "github.com/wandb/operator/api/v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const redisSourceSecretName = "external-redis" + +func redisSel(key string) corev1.SecretKeySelector { + return corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: redisSourceSecretName}, + Key: key, + } +} + +func TestWriteStateAddsCACertPathAndTLSWhenCACertPresent(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + require.NoError(t, apiv2.AddToScheme(scheme)) + + source := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: redisSourceSecretName, Namespace: "default"}, + Data: map[string][]byte{ + "Host": []byte("redis.example.com"), + "Port": []byte("6379"), + "Password": []byte("secret"), + "SslCa": []byte("---ca---"), + }, + } + wandb := &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{APIVersion: "apps.wandb.com/v2", Kind: "WeightsAndBiases"}, + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default"}, + Spec: apiv2.WeightsAndBiasesSpec{ + Redis: map[string]apiv2.RedisSpec{apiv2.DefaultInstanceName: { + ExternalRedis: &apiv2.RedisConnection{ + Host: redisSel("Host"), + Port: redisSel("Port"), + Password: redisSel("Password"), + SslCa: redisSel("SslCa"), + }, + }}, + }, + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(wandb, source).Build() + + conditions := WriteState(context.Background(), client, wandb, apiv2.DefaultInstanceName, wandb.Spec.Redis[apiv2.DefaultInstanceName].ExternalRedis) + require.Nil(t, conditions) + + written := &corev1.Secret{} + require.NoError(t, client.Get(context.Background(), types.NamespacedName{Name: ConnectionSecretName, Namespace: "default"}, written)) + data := redisConnectionData(written) + parsed, err := url.Parse(data["url"]) + require.NoError(t, err) + require.Equal(t, "redis", parsed.Scheme) + require.Equal(t, "redis.example.com:6379", parsed.Host) + require.Equal(t, "true", parsed.Query().Get("tls")) + require.Equal(t, caCertPath, parsed.Query().Get("caCertPath")) +} + +func redisConnectionData(secret *corev1.Secret) map[string]string { + out := map[string]string{} + for k, v := range secret.Data { + out[k] = string(v) + } + for k, v := range secret.StringData { + out[k] = v + } + return out +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/altinity_suite_test.go b/internal/controller/infra/managed/clickhouse/altinity/altinity_suite_test.go new file mode 100644 index 00000000..56f5def9 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/altinity_suite_test.go @@ -0,0 +1,13 @@ +package altinity + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestAltinity(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Altinity-ClickHouse Suite") +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/conn.go b/internal/controller/infra/managed/clickhouse/altinity/conn.go new file mode 100644 index 00000000..1a64641f --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/conn.go @@ -0,0 +1,117 @@ +package altinity + +import ( + "context" + "errors" + "fmt" + "net/url" + "strconv" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type clickhouseConnInfo struct { + Host string + TCPPort string + HTTPPort string + User string + Password string + Database string + Tls bool +} + +func (c *clickhouseConnInfo) toURL() string { + values := url.Values{ + "tls": []string{strconv.FormatBool(c.Tls)}, + } + clickhouseUrl := url.URL{ + Scheme: "clickhouse", + Host: fmt.Sprintf("%s:%s", c.Host, c.TCPPort), + User: url.UserPassword(c.User, c.Password), + Path: c.Database, + RawQuery: values.Encode(), + } + return clickhouseUrl.String() +} + +func writeClickHouseConnInfo( + ctx context.Context, + client client.Client, + owner client.Object, + nsnBuilder *NsNameBuilder, + connInfo *clickhouseConnInfo, +) ( + *apiv2.ClickHouseConnection, error, +) { + var err error + var found bool + var gvk schema.GroupVersionKind + var actual = &corev1.Secret{} + + if connInfo == nil { + return nil, errors.New("missing connection info") + } + + nsName := nsnBuilder.ConnectionNsName() + urlKey := "url" + + if found, err = common.GetResource( + ctx, client, nsName, AppConnTypeName, actual, + ); err != nil { + return nil, err + } + if !found { + actual = nil + } + + if gvk, err = client.GroupVersionKindFor(owner); err != nil { + return nil, fmt.Errorf("could not get GVK for owner: %w", err) + } + ref := metav1.OwnerReference{ + APIVersion: gvk.GroupVersion().String(), + Kind: gvk.Kind, + Name: owner.GetName(), + UID: owner.GetUID(), + Controller: ptr.To(false), + BlockOwnerDeletion: ptr.To(false), + } + + desired := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsName.Name, + Namespace: nsName.Namespace, + OwnerReferences: []metav1.OwnerReference{ref}, + }, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{ + urlKey: connInfo.toURL(), + "Host": connInfo.Host, + "TCPPort": connInfo.TCPPort, + "HTTPPort": connInfo.HTTPPort, + "User": connInfo.User, + "Password": connInfo.Password, + "Database": connInfo.Database, + }, + } + + if _, err = common.CrudResource(ctx, client, desired, actual); err != nil { + return nil, err + } + + localRef := corev1.LocalObjectReference{Name: nsName.Name} + return &apiv2.ClickHouseConnection{ + URL: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: urlKey, Optional: ptr.To(false)}, + Host: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Host", Optional: ptr.To(false)}, + HTTPPort: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "HTTPPort", Optional: ptr.To(false)}, + TCPPort: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "TCPPort", Optional: ptr.To(false)}, + Username: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "User", Optional: ptr.To(false)}, + Password: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Password", Optional: ptr.To(false)}, + Database: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Database", Optional: ptr.To(false)}, + }, nil +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/detach.go b/internal/controller/infra/managed/clickhouse/altinity/detach.go new file mode 100644 index 00000000..e5c0002a --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/detach.go @@ -0,0 +1,80 @@ +package altinity + +import ( + "context" + + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + chiv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func CheckDetached( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + wandbUID types.UID, +) []metav1.Condition { + nsnBuilder := createNsNameBuilder(specNamespacedName) + actual := &chiv1.ClickHouseInstallation{} + found, err := common.GetResource(ctx, cl, nsnBuilder.InstallationNsName(), ResourceTypeName, actual) + if err != nil || !found { + return nil + } + if !common.IsDetached(actual, wandbUID) { + return nil + } + return nil +} + +func DetachFinalizer( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + wandbOwner client.Object, +) error { + ctx, log := logx.WithSlog(ctx, logx.ClickHouse) + nsnBuilder := createNsNameBuilder(specNamespacedName) + + var actual = &chiv1.ClickHouseInstallation{} + found, err := common.GetResource(ctx, cl, nsnBuilder.InstallationNsName(), ResourceTypeName, actual) + if err != nil { + return err + } + if !found { + log.Info("abort detach finalizer: ClickHouseInstallation CR not found") + return nil + } + + if common.IsDetached(actual, wandbOwner.GetUID()) { + log.Debug("ClickHouseInstallation CR already detached") + return nil + } + + common.RemoveOwnerReference(actual, wandbOwner.GetUID()) + if err = cl.Update(ctx, actual); err != nil { + if errors.IsNotFound(err) { + return nil + } + log.Error("error detaching ClickHouseInstallation CR", logx.ErrAttr(err)) + return err + } + log.Info("detached ClickHouseInstallation CR", "name", actual.Name) + + secret := &corev1.Secret{} + found, err = common.GetResource(ctx, cl, nsnBuilder.ConnectionNsName(), "Secret", secret) + if err != nil || !found { + return err + } + common.RemoveOwnerReference(secret, wandbOwner.GetUID()) + if err = cl.Update(ctx, secret); err != nil && !errors.IsNotFound(err) { + log.Error("error detaching connection secret", logx.ErrAttr(err)) + return err + } + log.Info("detached connection secret", "name", nsnBuilder.ConnectionNsName().Name) + return nil +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/image_test.go b/internal/controller/infra/managed/clickhouse/altinity/image_test.go new file mode 100644 index 00000000..2094d312 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/image_test.go @@ -0,0 +1,31 @@ +package altinity + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/wandb/operator/pkg/wandb/manifest" +) + +var _ = Describe("ClickHouseImage", func() { + DescribeTable("resolves the image string", + func(img manifest.ImageRef, globalImageRegistry, expected string) { + Expect(ClickHouseImage(img, globalImageRegistry)).To(Equal(expected)) + }, + + // 1) No image from the manifest: fall back to the hardcoded default. + Entry("no manifest image", manifest.ImageRef{}, "", defaultClickHouseImage), + + // 2) Manifest supplies the image, no global registry: use it verbatim. + Entry("manifest image", + manifest.ImageRef{Registry: "docker.io", Repository: "altinity/clickhouse-server", Tag: "25.8"}, + "", + "docker.io/altinity/clickhouse-server:25.8"), + + // 3) Global image registry is prepended in front of the manifest image. + Entry("manifest image, global registry set", + manifest.ImageRef{Registry: "docker.io", Repository: "altinity/clickhouse-server", Tag: "25.8"}, + "myregistry.io", + "myregistry.io/docker.io/altinity/clickhouse-server:25.8"), + ) +}) diff --git a/internal/controller/infra/managed/clickhouse/altinity/keeper/keeper_suite_test.go b/internal/controller/infra/managed/clickhouse/altinity/keeper/keeper_suite_test.go new file mode 100644 index 00000000..de4aa549 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/keeper/keeper_suite_test.go @@ -0,0 +1,13 @@ +package keeper + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestKeeper(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ClickHouse-Keeper Suite") +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/keeper/naming.go b/internal/controller/infra/managed/clickhouse/altinity/keeper/naming.go new file mode 100644 index 00000000..db983209 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/keeper/naming.go @@ -0,0 +1,35 @@ +package keeper + +import ( + "fmt" +) + +// chkNameSuffix is terse on purpose: it comes out of the DNS-1123 budget of +// every derived per-host name (see PerHostConfigVolumeName). +const chkNameSuffix = "-chk" + +// InstallationName derives the Keeper CR name from the base shared with the +// installation ("<base>-chi" / "<base>-chk"). Re-derived every reconcile: +// changing the scheme after managed ClickHouse ships needs a migration path. +func InstallationName(baseName string) string { + return baseName + chkNameSuffix +} + +// ClientServiceName is the Altinity-created client Service name ("keeper-<chk-name>"). +func ClientServiceName(baseName string) string { + return "keeper-" + InstallationName(baseName) +} + +// ClientServiceFQDN is the in-cluster DNS the CHI's <zookeeper> config points at. +func ClientServiceFQDN(namespace, baseName string) string { + return fmt.Sprintf("%s.%s.svc.cluster.local", ClientServiceName(baseName), namespace) +} + +// PerHostConfigVolumeName mirrors the Altinity operator's per-host +// ConfigMap/StatefulSet-volume name (pkg/model/chk/namer/patterns.go). The +// longest derived name, and a DNS-1123 label: past 63 chars the apiserver +// rejects the StatefulSet and Altinity retries without surfacing the failure. +func PerHostConfigVolumeName(baseName string, shardOrdinal, replicaOrdinal int) string { + return fmt.Sprintf("chk-%s-deploy-confd-%s-%d-%d", + InstallationName(baseName), ClusterName, shardOrdinal, replicaOrdinal) +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/keeper/read.go b/internal/controller/infra/managed/clickhouse/altinity/keeper/read.go new file mode 100644 index 00000000..ed24b015 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/keeper/read.go @@ -0,0 +1,104 @@ +package keeper + +import ( + "context" + "fmt" + + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + chkv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// ReadState reports Keeper pod readiness via KeeperReportedReadyType, which gates +// ClickHouse readiness. +func ReadState( + ctx context.Context, + cl client.Client, + keeperNsName types.NamespacedName, +) []metav1.Condition { + ctx, _ = logx.WithSlog(ctx, logx.ClickHouse) + var actual = &chkv1.ClickHouseKeeperInstallation{} + + found, err := common.GetResource(ctx, cl, keeperNsName, ResourceTypeName, actual) + if err != nil { + return []metav1.Condition{{ + Type: KeeperReportedReadyType, + Status: metav1.ConditionUnknown, + Reason: common.ApiErrorReason, + }} + } + if !found { + return []metav1.Condition{{ + Type: KeeperReportedReadyType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }} + } + + podsRunning, err := keeperPodsRunningStatus(ctx, cl, keeperNsName.Namespace, actual) + if err != nil { + return []metav1.Condition{{ + Type: KeeperReportedReadyType, + Status: metav1.ConditionUnknown, + Reason: common.ApiErrorReason, + }} + } + + return computeKeeperReadyCondition(ctx, podsRunning) +} + +func keeperPodsRunningStatus( + ctx context.Context, cl client.Client, namespace string, chk *chkv1.ClickHouseKeeperInstallation, +) (map[string]bool, error) { + result := make(map[string]bool) + if chk == nil || chk.Status == nil { + return result, nil + } + for _, podName := range chk.Status.Pods { + var pod = &corev1.Pod{} + nsName := types.NamespacedName{Namespace: namespace, Name: podName} + found, err := common.GetResource(ctx, cl, nsName, "KeeperPod", pod) + if err != nil { + return result, err + } + result[podName] = found && common.PodReady(pod) + } + return result, nil +} + +func computeKeeperReadyCondition(ctx context.Context, podsRunning map[string]bool) []metav1.Condition { + log := logx.GetSlog(ctx) + + var runningCount, podCount int + for _, isRunning := range podsRunning { + podCount++ + if isRunning { + runningCount++ + } + } + log.Info("Keeper pods status", "running", runningCount, "total", podCount) + + status := metav1.ConditionUnknown + reason := common.UnknownReason + message := "" + switch { + case podCount > 0 && podCount == runningCount: + status = metav1.ConditionTrue + reason = common.ResourceExistsReason + case podCount > 0: + status = metav1.ConditionFalse + reason = common.NoResourceReason + message = fmt.Sprintf("%d of %d keeper pods running", runningCount, podCount) + } + + return []metav1.Condition{{ + Type: KeeperReportedReadyType, + Status: status, + Reason: reason, + Message: message, + }} +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/keeper/read_test.go b/internal/controller/infra/managed/clickhouse/altinity/keeper/read_test.go new file mode 100644 index 00000000..5db34fb3 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/keeper/read_test.go @@ -0,0 +1,29 @@ +package keeper + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("Keeper readiness", func() { + It("is ready when all pods are running", func() { + conds := computeKeeperReadyCondition(context.Background(), map[string]bool{"a": true, "b": true, "c": true}) + Expect(conds).To(HaveLen(1)) + Expect(conds[0].Type).To(Equal(KeeperReportedReadyType)) + Expect(conds[0].Status).To(Equal(metav1.ConditionTrue)) + }) + + It("is not ready when some pods are not running", func() { + conds := computeKeeperReadyCondition(context.Background(), map[string]bool{"a": true, "b": false, "c": true}) + Expect(conds[0].Status).To(Equal(metav1.ConditionFalse)) + Expect(conds[0].Message).To(ContainSubstring("2 of 3")) + }) + + It("is unknown when no pods are reported yet", func() { + conds := computeKeeperReadyCondition(context.Background(), map[string]bool{}) + Expect(conds[0].Status).To(Equal(metav1.ConditionUnknown)) + }) +}) diff --git a/internal/controller/infra/managed/clickhouse/altinity/keeper/spec.go b/internal/controller/infra/managed/clickhouse/altinity/keeper/spec.go new file mode 100644 index 00000000..812c757c --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/keeper/spec.go @@ -0,0 +1,157 @@ +package keeper + +import ( + "context" + "fmt" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + "github.com/wandb/operator/pkg/utils" + chkv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1" + chiv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" +) + +// ToKeeperVendorSpec builds the ClickHouseKeeperInstallation CR that coordinates +// ReplicatedMergeTree replication. nsName comes from altinity.KeeperNsName — +// this package never sees the "-chi"-suffixed spec name. +func ToKeeperVendorSpec( + ctx context.Context, + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedClickHouseSpec, + scheme *runtime.Scheme, + nsName types.NamespacedName, +) (*chkv1.ClickHouseKeeperInstallation, error) { + _, log := logx.WithSlog(ctx, logx.ClickHouse) + if spec == nil { + return nil, nil + } + + // Keeper sizing comes from the server manifest (clickhouseKeeper) or CR; there + // are no operator-side defaults, so fail loudly if the storage size is missing. + storageQuantity, err := resource.ParseQuantity(spec.Keeper.StorageSize) + if err != nil { + return nil, fmt.Errorf("invalid keeper storageSize %q (expected from the server manifest's clickhouseKeeper sizing): %w", spec.Keeper.StorageSize, err) + } + + labels := common.BuildWandbLabels(wandb, KeeperModuleName) + + podSpec := corev1.PodSpec{ + SecurityContext: keeperPodSecurityContext(), + Affinity: wandb.GetAffinity(spec.ManagedInfraSpec), + Tolerations: *wandb.GetTolerations(spec.ManagedInfraSpec), + Containers: []corev1.Container{ + { + Name: keeperContainerName, + Image: KeeperImage, + SecurityContext: keeperContainerSecurityContext(), + }, + }, + } + if len(spec.Keeper.Config.Resources.Requests) > 0 || len(spec.Keeper.Config.Resources.Limits) > 0 { + podSpec.Containers[0].Resources = corev1.ResourceRequirements{ + Requests: spec.Keeper.Config.Resources.Requests, + Limits: spec.Keeper.Config.Resources.Limits, + } + } + + chk := &chkv1.ClickHouseKeeperInstallation{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsName.Name, + Namespace: nsName.Namespace, + Labels: labels, + }, + Spec: chkv1.ChkSpec{ + Configuration: &chkv1.Configuration{ + Clusters: []*chkv1.Cluster{ + { + Name: ClusterName, + Layout: &chkv1.ChkClusterLayout{ + ReplicasCount: int(spec.Keeper.Replicas), + }, + }, + }, + }, + Defaults: &chiv1.Defaults{ + Templates: &chiv1.TemplatesList{ + PodTemplate: podTemplateName, + DataVolumeClaimTemplate: volumeTemplateName, + }, + }, + Templates: &chiv1.Templates{ + PodTemplates: []chiv1.PodTemplate{ + { + Name: podTemplateName, + ObjectMeta: metav1.ObjectMeta{Labels: labels}, + Spec: podSpec, + }, + }, + VolumeClaimTemplates: []chiv1.VolumeClaimTemplate{ + { + Name: volumeTemplateName, + ObjectMeta: metav1.ObjectMeta{Labels: labels}, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceStorage: storageQuantity}, + }, + }, + }, + }, + }, + }, + } + + if err := ctrl.SetControllerReference(wandb, chk, scheme); err != nil { + log.Error("failed to set owner reference on CHK CR", logx.ErrAttr(err)) + return nil, fmt.Errorf("failed to set owner reference: %w", err) + } + + return chk, nil +} + +// BuildWandbKeeperLabels returns the standard W&B labels for Keeper resources. +func BuildWandbKeeperLabels(wandb *apiv2.WeightsAndBiases) map[string]string { + return common.BuildWandbLabels(wandb, KeeperModuleName) +} + +func keeperPodSecurityContext() *corev1.PodSecurityContext { + if utils.IsOpenShift() { + return &corev1.PodSecurityContext{ + RunAsNonRoot: ptr.To(true), + SeccompProfile: runtimeDefaultSeccompProfile(), + } + } + return &corev1.PodSecurityContext{ + RunAsUser: ptr.To(keeperRunAsUser), + RunAsGroup: ptr.To(keeperRunAsGroup), + RunAsNonRoot: ptr.To(true), + FSGroup: ptr.To(keeperFSGroup), + SeccompProfile: runtimeDefaultSeccompProfile(), + } +} + +func keeperContainerSecurityContext() *corev1.SecurityContext { + sc := &corev1.SecurityContext{ + RunAsNonRoot: ptr.To(true), + AllowPrivilegeEscalation: ptr.To(false), + Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"ALL"}}, + SeccompProfile: runtimeDefaultSeccompProfile(), + } + if !utils.IsOpenShift() { + sc.RunAsUser = ptr.To(keeperRunAsUser) + sc.RunAsGroup = ptr.To(keeperRunAsGroup) + } + return sc +} + +func runtimeDefaultSeccompProfile() *corev1.SeccompProfile { + return &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault} +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/keeper/spec_test.go b/internal/controller/infra/managed/clickhouse/altinity/keeper/spec_test.go new file mode 100644 index 00000000..78319ff4 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/keeper/spec_test.go @@ -0,0 +1,119 @@ +package keeper + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/pkg/utils" + chkv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1" + chiv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("Keeper vendor spec", func() { + BeforeEach(func() { + utils.SetOpenShiftMode(false) + }) + + It("builds a CHK with explicit replicas, storage, and a hardened pod", func() { + wandb := keeperWandb() + wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.Keeper = apiv2.ClickHouseKeeperSpec{ + Replicas: 5, + StorageSize: "20Gi", + Config: apiv2.ClickHouseConfig{ + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("250m")}, + }, + }, + } + + chk, err := ToKeeperVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, keeperScheme(), keeperNsName()) + Expect(err).NotTo(HaveOccurred()) + Expect(chk).NotTo(BeNil()) + Expect(chk.Name).To(Equal("clickhouse-chk")) + Expect(chk.Namespace).To(Equal("wandb")) + + Expect(chk.Spec.Configuration.Clusters).To(HaveLen(1)) + Expect(chk.Spec.Configuration.Clusters[0].Layout.ReplicasCount).To(Equal(5)) + + Expect(chk.Spec.Templates.VolumeClaimTemplates).To(HaveLen(1)) + storage := chk.Spec.Templates.VolumeClaimTemplates[0].Spec.Resources.Requests[corev1.ResourceStorage] + Expect(storage).To(Equal(resource.MustParse("20Gi"))) + + Expect(chk.Spec.Templates.PodTemplates).To(HaveLen(1)) + container := chk.Spec.Templates.PodTemplates[0].Spec.Containers[0] + Expect(container.Image).To(Equal(KeeperImage)) + Expect(container.Name).To(Equal(keeperContainerName)) + Expect(container.Resources.Requests[corev1.ResourceCPU]).To(Equal(resource.MustParse("250m"))) + + sc := chk.Spec.Templates.PodTemplates[0].Spec.SecurityContext + Expect(sc).NotTo(BeNil()) + Expect(sc.RunAsUser).NotTo(BeNil()) + Expect(*sc.RunAsUser).To(Equal(keeperRunAsUser)) + Expect(sc.RunAsNonRoot).NotTo(BeNil()) + Expect(*sc.RunAsNonRoot).To(BeTrue()) + }) + + It("errors when keeper storage size is unset (no operator defaults)", func() { + wandb := keeperWandb() + wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.Keeper = apiv2.ClickHouseKeeperSpec{} + _, err := ToKeeperVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, keeperScheme(), keeperNsName()) + Expect(err).To(HaveOccurred()) + }) + + It("omits fixed IDs in OpenShift mode", func() { + utils.SetOpenShiftMode(true) + wandb := keeperWandb() + chk, err := ToKeeperVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, keeperScheme(), keeperNsName()) + Expect(err).NotTo(HaveOccurred()) + sc := chk.Spec.Templates.PodTemplates[0].Spec.SecurityContext + Expect(sc.RunAsUser).To(BeNil()) + Expect(sc.RunAsNonRoot).NotTo(BeNil()) + Expect(*sc.RunAsNonRoot).To(BeTrue()) + }) +}) + +// keeperNsName mirrors what altinity.KeeperNsName derives for keeperWandb(). +func keeperNsName() types.NamespacedName { + return types.NamespacedName{Namespace: "wandb", Name: InstallationName("clickhouse")} +} + +func keeperScheme() *runtime.Scheme { + scheme := runtime.NewScheme() + Expect(apiv2.AddToScheme(scheme)).To(Succeed()) + Expect(chiv1.AddToScheme(scheme)).To(Succeed()) + Expect(chkv1.AddToScheme(scheme)).To(Succeed()) + return scheme +} + +func keeperWandb() *apiv2.WeightsAndBiases { + tolerations := []corev1.Toleration{} + return &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiv2.GroupVersion.String(), + Kind: "WeightsAndBiases", + }, + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "wandb"}, + Spec: apiv2.WeightsAndBiasesSpec{ + Tolerations: &tolerations, + ClickHouse: map[string]apiv2.ClickHouseSpec{ + apiv2.DefaultInstanceName: { + ManagedClickHouse: &apiv2.ManagedClickHouseSpec{ + Name: "clickhouse", + Namespace: "wandb", + Keeper: apiv2.ClickHouseKeeperSpec{ + Replicas: 3, + StorageSize: "10Gi", + }, + }, + }, + }, + }, + } +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/keeper/values.go b/internal/controller/infra/managed/clickhouse/altinity/keeper/values.go new file mode 100644 index 00000000..c5bc4f6f --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/keeper/values.go @@ -0,0 +1,29 @@ +package keeper + +const ( + // KeeperModuleName is the W&B component label value for Keeper resources. + KeeperModuleName = "clickhouse-keeper" + + // KeeperImage pins the Keeper image to the managed ClickHouse server version. + KeeperImage = "altinity/clickhouse-keeper:25.8.16.10002.altinitystable" + + // KeeperClientPort is the ZooKeeper-compatible client port. + KeeperClientPort = 2181 + + // ClusterName is the name of the single Keeper cluster. + ClusterName = "default" + + // KeeperCustomResourceType is the condition type reported for the CHK CR. + KeeperCustomResourceType = "KeeperCustomResource" + + // KeeperReportedReadyType reports Keeper pod readiness; it gates ClickHouse readiness. + KeeperReportedReadyType = "KeeperReportedReady" + + podTemplateName = "keeper-pod-template" + volumeTemplateName = "keeper-data-volume" + keeperContainerName = "clickhouse-keeper" + + keeperRunAsUser int64 = 101 + keeperRunAsGroup int64 = 101 + keeperFSGroup int64 = 101 +) diff --git a/internal/controller/infra/managed/clickhouse/altinity/keeper/write.go b/internal/controller/infra/managed/clickhouse/altinity/keeper/write.go new file mode 100644 index 00000000..2185b498 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/keeper/write.go @@ -0,0 +1,71 @@ +package keeper + +import ( + "context" + + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + chkv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// ResourceTypeName is the kind used for logging/error reporting of the CHK CR. +const ResourceTypeName = "ClickHouseKeeperInstallation" + +// WriteState create-or-updates the CHK, setting only the fields we own (spec, +// labels, owner refs) and preserving the Altinity-managed finalizer/status. It +// compares owned fields via JSON, never the vendored status — whose uint64 and +// unexported fields panic controllerutil's reflective diff/copy. +func WriteState( + ctx context.Context, + cl client.Client, + keeperNsName types.NamespacedName, + desired *chkv1.ClickHouseKeeperInstallation, +) []metav1.Condition { + ctx, _ = logx.WithSlog(ctx, logx.ClickHouse) + + obj := &chkv1.ClickHouseKeeperInstallation{ + ObjectMeta: metav1.ObjectMeta{Name: keeperNsName.Name, Namespace: keeperNsName.Namespace}, + } + + op, err := common.WriteOwnedFields(ctx, cl, obj, + func(o *chkv1.ClickHouseKeeperInstallation) { applyOwnedKeeper(o, desired) }, + keeperOwnedEqual, + ) + if err != nil { + return []metav1.Condition{ + {Type: KeeperCustomResourceType, Status: metav1.ConditionUnknown, Reason: common.ApiErrorReason}, + } + } + + if op == controllerutil.OperationResultCreated { + return []metav1.Condition{ + {Type: KeeperCustomResourceType, Status: metav1.ConditionFalse, Reason: common.PendingCreateReason}, + } + } + return []metav1.Condition{ + {Type: KeeperCustomResourceType, Status: metav1.ConditionTrue, Reason: common.ResourceExistsReason}, + } +} + +func applyOwnedKeeper(obj, desired *chkv1.ClickHouseKeeperInstallation) { + labels := obj.GetLabels() + if labels == nil { + labels = map[string]string{} + } + for k, v := range desired.GetLabels() { + labels[k] = v + } + obj.SetLabels(labels) + obj.SetOwnerReferences(desired.GetOwnerReferences()) + obj.Spec = desired.Spec +} + +func keeperOwnedEqual(a, b *chkv1.ClickHouseKeeperInstallation) bool { + return common.JSONEqual(a.Spec, b.Spec) && + common.JSONEqual(a.Labels, b.Labels) && + common.JSONEqual(a.OwnerReferences, b.OwnerReferences) +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/naming.go b/internal/controller/infra/managed/clickhouse/altinity/naming.go new file mode 100644 index 00000000..9ffe32e5 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/naming.go @@ -0,0 +1,153 @@ +package altinity + +import ( + "fmt" + "strings" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/managed/clickhouse/altinity/keeper" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation" +) + +type NsNameBuilder struct { + baseNsName types.NamespacedName +} + +func CreateNsNameBuilder(baseNsName types.NamespacedName) *NsNameBuilder { + return &NsNameBuilder{ + baseNsName: baseNsName, + } +} + +func (n *NsNameBuilder) Namespace() string { + return n.baseNsName.Namespace +} + +func (n *NsNameBuilder) SpecName() string { + return n.baseNsName.Name +} + +func (n *NsNameBuilder) InstallationName() string { + return n.SpecName() +} + +func (n *NsNameBuilder) InstallationNsName() types.NamespacedName { + return types.NamespacedName{ + Namespace: n.Namespace(), + Name: n.InstallationName(), + } +} + +func (n *NsNameBuilder) ClusterName() string { + name := n.SpecName() + if len(name) > 15 { + name = name[:15] + } + // Trim trailing hyphens + for len(name) > 0 && name[len(name)-1] == '-' { + name = name[:len(name)-1] + } + return name +} + +func (n *NsNameBuilder) VolumeTemplateName() string { + return fmt.Sprintf("%s-voltempl", n.SpecName()) +} + +func (n *NsNameBuilder) PodTemplateName() string { + return fmt.Sprintf("%s-podtempl", n.SpecName()) +} + +func (n *NsNameBuilder) ConnectionName() string { + return fmt.Sprintf("%s-connection", n.SpecName()) +} + +func (n *NsNameBuilder) ConnectionNsName() types.NamespacedName { + return types.NamespacedName{ + Namespace: n.Namespace(), + Name: n.ConnectionName(), + } +} + +// Internal function for backward compatibility within the package +func createNsNameBuilder(baseNsName types.NamespacedName) *NsNameBuilder { + return CreateNsNameBuilder(baseNsName) +} + +const ( + // chiClusterName is the single cluster the CHI defines. + chiClusterName = "default" + + // defaultNameSuffix is appended to the CR name by the defaulting webhook; + // terse to leave the CR name as much of the derived-name budget as possible. + defaultNameSuffix = "-chi" + + // maxExpectedHostOrdinal reserves two digits each for shard and replica + // ordinals (largest expected cluster ~100 pods), so a persisted name can + // never overflow when the cluster is scaled up later. + maxExpectedHostOrdinal = 99 +) + +// baseName strips the "-chi" suffix; the Keeper builds its names on the base. +func baseName(specName string) string { + return strings.TrimSuffix(specName, defaultNameSuffix) +} + +// KeeperNsName is the Keeper CR's namespaced name for a managed CH spec. +func KeeperNsName(spec *apiv2.ManagedClickHouseSpec) types.NamespacedName { + return types.NamespacedName{ + Namespace: spec.Namespace, + Name: keeper.InstallationName(baseName(spec.Name)), + } +} + +// perHostConfigVolumeName mirrors the Altinity operator's per-host +// ConfigMap/StatefulSet-volume name for a CHI (pkg/model/chi/namer/patterns.go); +// the longest CHI-derived name and a DNS-1123 label. +func perHostConfigVolumeName(specName string, shardOrdinal, replicaOrdinal int) string { + return fmt.Sprintf("chi-%s-deploy-confd-%s-%d-%d", specName, chiClusterName, shardOrdinal, replicaOrdinal) +} + +// MaxSpecNameLength is the longest defaulted ("<base>-chi") spec name whose +// derived names all fit DNS-1123 labels; Keeper names build on the base, so +// their room extends by the suffix the base gives back. +func MaxSpecNameLength() int { + chiRoom := validation.DNS1123LabelMaxLength - + len(perHostConfigVolumeName("", maxExpectedHostOrdinal, maxExpectedHostOrdinal)) + chkRoom := validation.DNS1123LabelMaxLength - + len(keeper.PerHostConfigVolumeName("", maxExpectedHostOrdinal, maxExpectedHostOrdinal)) + len(defaultNameSuffix) + return min(chiRoom, chkRoom) +} + +// DefaultSpecName derives the managed ClickHouse name for a CR instance, +// shortening it when the plain form would overflow the derived-name budget. +func DefaultSpecName(crName, instanceKey string) string { + return common.FitDefaultInfraName(common.InstanceBaseName(crName, instanceKey), defaultNameSuffix, MaxSpecNameLength()) +} + +// ValidateDerivedNames reports why a spec name cannot be deployed: derived +// per-host volume names must fit DNS-1123 labels, and the Altinity operator +// wedges silently when they don't. Nil when every derived name fits. +func ValidateDerivedNames(spec *apiv2.ManagedClickHouseSpec) error { + for _, derived := range []string{ + keeper.PerHostConfigVolumeName(baseName(spec.Name), maxExpectedHostOrdinal, maxExpectedHostOrdinal), + perHostConfigVolumeName(spec.Name, maxExpectedHostOrdinal, maxExpectedHostOrdinal), + } { + // derived length grows 1:1 with the name, so excess → max usable length + if over := len(derived) - validation.DNS1123LabelMaxLength; over > 0 { + return fmt.Errorf( + "managed ClickHouse name %q cannot be deployed: the Altinity operator derives object name %q from it, which exceeds the %d-character DNS-1123 label limit; use at most %d characters, e.g. by shortening the CR name or setting spec.clickhouse.managedClickhouse.name", + spec.Name, derived, validation.DNS1123LabelMaxLength, len(spec.Name)-over, + ) + } + if errs := validation.IsDNS1123Label(derived); len(errs) > 0 { + return fmt.Errorf( + "managed ClickHouse name %q cannot be deployed: the Altinity operator derives object name %q from it, which is not a valid DNS-1123 label (%s)", + spec.Name, derived, strings.Join(errs, "; "), + ) + } + } + return nil +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/naming_test.go b/internal/controller/infra/managed/clickhouse/altinity/naming_test.go new file mode 100644 index 00000000..2f8a02f2 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/naming_test.go @@ -0,0 +1,87 @@ +package altinity + +import ( + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + "k8s.io/apimachinery/pkg/util/validation" +) + +var _ = Describe("managed ClickHouse naming", func() { + Describe("KeeperNsName", func() { + It("pairs the Keeper with the installation via the shared base name", func() { + spec := &apiv2.ManagedClickHouseSpec{Name: "wandb-legacy-overrides-v1-chi", Namespace: "wandb"} + Expect(KeeperNsName(spec).Name).To(Equal("wandb-legacy-overrides-v1-chk")) + }) + + It("appends -chk to explicit names without the default suffix", func() { + spec := &apiv2.ManagedClickHouseSpec{Name: "myclickhouse", Namespace: "wandb"} + Expect(KeeperNsName(spec).Name).To(Equal("myclickhouse-chk")) + }) + }) + + Describe("MaxSpecNameLength", func() { + It("costs the Keeper chain no more than the CHI chain, thanks to the suffix swap", func() { + chiRoom := validation.DNS1123LabelMaxLength - + len(perHostConfigVolumeName("", maxExpectedHostOrdinal, maxExpectedHostOrdinal)) + Expect(MaxSpecNameLength()).To(Equal(chiRoom)) + }) + }) + + Describe("DefaultSpecName", func() { + It("keeps the plain '<cr>-chi' for CR names that fit", func() { + Expect(DefaultSpecName("wandb", apiv2.DefaultInstanceName)).To(Equal("wandb-chi")) + // 25 chars — wedged the old "-clickhouse"/"-keeper" naming + Expect(DefaultSpecName("wandb-legacy-overrides-v1", apiv2.DefaultInstanceName)).To(Equal("wandb-legacy-overrides-v1-chi")) + }) + + It("keys non-default instances before the suffix so derivations still work", func() { + name := DefaultSpecName("wandb", "analytics") + + Expect(name).To(Equal("wandb-analytics-chi")) + spec := &apiv2.ManagedClickHouseSpec{Name: name} + Expect(KeeperNsName(spec).Name).To(Equal("wandb-analytics-chk")) + Expect(ValidateDerivedNames(spec)).To(Succeed()) + }) + + It("derives a deployable name for CR names the plain default would wedge", func() { + // 32 chars: "<cr>-chi" would overflow the per-host volume names + name := DefaultSpecName("wandb-integration-environments-2", apiv2.DefaultInstanceName) + + Expect(name).To(HaveSuffix("-chi")) + Expect(ValidateDerivedNames(&apiv2.ManagedClickHouseSpec{Name: name})).To(Succeed()) + }) + }) + + Describe("ValidateDerivedNames", func() { + It("accepts a defaulted name", func() { + Expect(ValidateDerivedNames(&apiv2.ManagedClickHouseSpec{Name: "wandb-chi"})).To(Succeed()) + }) + + It("rejects a name whose Keeper volume name exceeds the DNS-1123 label limit", func() { + err := ValidateDerivedNames(&apiv2.ManagedClickHouseSpec{Name: "wandb-legacy-overrides-v1-clickhouse"}) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("chk-wandb-legacy-overrides-v1-clickhouse-chk-deploy-confd")) + Expect(err.Error()).To(ContainSubstring("spec.clickhouse.managedClickhouse.name")) + }) + + It("reserves ordinal digits so a name at the budget survives scaling to 100 pods", func() { + atBudget := &apiv2.ManagedClickHouseSpec{ + Name: strings.Repeat("a", MaxSpecNameLength()-len(defaultNameSuffix)) + defaultNameSuffix, + Replicas: 99, + } + atBudget.Keeper.Replicas = 99 + Expect(ValidateDerivedNames(atBudget)).To(Succeed()) + + atBudget.Name = "a" + atBudget.Name + Expect(ValidateDerivedNames(atBudget)).To(HaveOccurred()) + }) + + It("rejects characters that are invalid in derived label names", func() { + Expect(ValidateDerivedNames(&apiv2.ManagedClickHouseSpec{Name: "wandb.prod"})).To(HaveOccurred()) + }) + }) +}) diff --git a/internal/controller/infra/managed/clickhouse/altinity/objectstorage.go b/internal/controller/infra/managed/clickhouse/altinity/objectstorage.go new file mode 100644 index 00000000..3b957677 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/objectstorage.go @@ -0,0 +1,208 @@ +package altinity + +import ( + "context" + "fmt" + "strconv" + "strings" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + chtypes "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + // StoragePolicyName is the object-store-backed policy, set server-wide so all + // MergeTree tables land in the bucket without per-table DDL. + StoragePolicyName = "s3_main" + + // DefaultObjectStoragePrefix is the in-bucket prefix when unset (trailing slash matters). + DefaultObjectStoragePrefix = "clickhouse/" + + s3DiskName = "s3_disk" + // Must sort after s3DiskName ('/' < '_'): the renderer emits <disks> in sorted + // order and ClickHouse requires the wrapped disk before its cache disk. + s3CacheDiskName = "s3_disk_cache" + s3MetadataPath = "/var/lib/clickhouse/disks/s3_disk/" + s3CachePath = "/var/lib/clickhouse/disks/s3_disk_cache/" + + // storageConfigKey renders to the <storage_configuration> config section. + storageConfigKey = "storage_configuration" +) + +// ObjectStorageConn holds the resolved bucket connection used to configure the +// S3-backed disk: endpoint/region as literals, credentials as secret references. +type ObjectStorageConn struct { + // Endpoint is the full http(s) URL incl. bucket + prefix, trailing slash. + Endpoint string + Region string + // UseEnvCredentials uses ambient creds (IAM role) instead of access keys. + UseEnvCredentials bool + AccessKeyRef corev1.SecretKeySelector + SecretKeyRef corev1.SecretKeySelector +} + +// ResolveObjectStorage reads the object-store connection's secret key references +// (which may span multiple secrets) and derives the S3 disk details. +func ResolveObjectStorage( + ctx context.Context, + cl client.Client, + spec *apiv2.ManagedClickHouseSpec, + conn *apiv2.ObjectStoreConnection, +) (*ObjectStorageConn, error) { + if spec == nil { + return nil, nil + } + if conn == nil { + return nil, fmt.Errorf("object store connection is not available yet") + } + + r := &utils.ConnSecretResolver{Client: cl, Namespace: spec.Namespace, Cache: map[string]*corev1.Secret{}} + + bucket, err := r.Value(ctx, conn.Bucket) + if err != nil { + return nil, err + } + if bucket == "" { + return nil, fmt.Errorf("object store connection has no bucket reference") + } + + host, err := r.Value(ctx, conn.Endpoint) + if err != nil { + return nil, err + } + port, err := r.Value(ctx, conn.Port) + if err != nil { + return nil, err + } + region, err := r.Value(ctx, conn.Region) + if err != nil { + return nil, err + } + accessKey, err := r.Value(ctx, conn.AccessKey) + if err != nil { + return nil, err + } + + tlsEnabledString, err := r.Value(ctx, conn.TlsEnabled) + if err != nil { + return nil, err + } + + tlsEnabled, err := strconv.ParseBool(tlsEnabledString) + if err != nil { + tlsEnabled = false + } + + // The connection has no scheme reference; honor one advertised in the + // endpoint value, otherwise let buildEndpoint infer from spec.Insecure. + scheme := "" + if i := strings.Index(host, "://"); i >= 0 { + scheme, host = host[:i], host[i+len("://"):] + } + + endpoint, err := buildEndpoint(scheme, host, port, bucket, region, objectStoragePrefix(spec), tlsEnabled) + if err != nil { + return nil, err + } + + return &ObjectStorageConn{ + Endpoint: endpoint, + Region: region, + UseEnvCredentials: accessKey == "", + AccessKeyRef: conn.AccessKey, + SecretKeyRef: conn.SecretKey, + }, nil +} + +func objectStoragePrefix(spec *apiv2.ManagedClickHouseSpec) string { + return normalizePrefix(spec.ObjectStorage.Prefix) +} + +// normalizePrefix strips leading slashes and ensures one trailing slash, defaulting when empty. +func normalizePrefix(prefix string) string { + prefix = strings.TrimSpace(prefix) + if prefix == "" { + return DefaultObjectStoragePrefix + } + prefix = strings.Trim(prefix, "/") + return prefix + "/" +} + +// buildEndpoint builds the S3 disk endpoint: path-style for a custom host, else +// the AWS virtual-hosted URL derived from the region. +func buildEndpoint(scheme, host, port, bucket, region, prefix string, secure bool) (string, error) { + if host != "" { + if scheme == "" { + scheme = "http" + if secure { + scheme = "https" + } + } + hostport := host + if port != "" { + hostport = host + ":" + port + } + return fmt.Sprintf("%s://%s/%s/%s", scheme, hostport, bucket, prefix), nil + } + + if region == "" { + return "", fmt.Errorf("object store has no Host and no Region; cannot derive an S3 endpoint") + } + return fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", bucket, region, prefix), nil +} + +// applyStorageConfiguration adds the <storage_configuration> (S3 disk + cache + +// policy) to settings. Credentials are secret references so the Altinity +// operator injects them via from_env rather than as plaintext. +// TODO(dpanzella): Currently only supports S3 compatible storage, add support for Azure and GCS +func applyStorageConfiguration(settings *v1.Settings, oc *ObjectStorageConn, cacheMaxSizeBytes int64) { + disk := diskKey(s3DiskName) + settings.Set(disk("type"), v1.NewSettingScalar("s3")) + settings.Set(disk("endpoint"), v1.NewSettingScalar(oc.Endpoint)) + settings.Set(disk("metadata_path"), v1.NewSettingScalar(s3MetadataPath)) + if oc.Region != "" { + settings.Set(disk("region"), v1.NewSettingScalar(oc.Region)) + } + if oc.UseEnvCredentials { + settings.Set(disk("use_environment_credentials"), v1.NewSettingScalar("true")) + } else { + settings.Set(disk("access_key_id"), secretSetting(oc.AccessKeyRef)) + settings.Set(disk("secret_access_key"), secretSetting(oc.SecretKeyRef)) + } + + cache := diskKey(s3CacheDiskName) + settings.Set(cache("type"), v1.NewSettingScalar("cache")) + settings.Set(cache("disk"), v1.NewSettingScalar(s3DiskName)) + settings.Set(cache("path"), v1.NewSettingScalar(s3CachePath)) + settings.Set(cache("max_size"), v1.NewSettingScalar(strconv.FormatInt(cacheMaxSizeBytes, 10))) + + settings.Set( + storageConfigKey+"/policies/"+StoragePolicyName+"/volumes/main/disk", + v1.NewSettingScalar(s3CacheDiskName), + ) + + // Server-wide default so W&B tables use the bucket without per-table DDL. + // system_*_log tables ship a predefined <engine> (which can't take a separate + // storage_policy), so they inherit this and live in the bucket too. + settings.Set("merge_tree/storage_policy", v1.NewSettingScalar(StoragePolicyName)) +} + +// diskKey builds settings paths for a named disk, e.g. +// diskKey("s3_disk")("type") -> "storage_configuration/disks/s3_disk/type". +func diskKey(name string) func(string) string { + prefix := storageConfigKey + "/disks/" + name + "/" + return func(field string) string { return prefix + field } +} + +// secretSetting builds a setting sourced from a Kubernetes secret; the Altinity +// operator wires it as a pod env var + from_env. +func secretSetting(ref corev1.SecretKeySelector) *v1.Setting { + r := ref + return v1.NewSettingSource(&v1.SettingSource{ + ValueFrom: &chtypes.DataSource{SecretKeyRef: &r}, + }) +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/objectstorage_test.go b/internal/controller/infra/managed/clickhouse/altinity/objectstorage_test.go new file mode 100644 index 00000000..982ce3f4 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/objectstorage_test.go @@ -0,0 +1,109 @@ +package altinity + +import ( + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + corev1 "k8s.io/api/core/v1" +) + +var _ = Describe("Object storage endpoint", func() { + It("uses the scheme reported by the connection for a custom host", func() { + ep, err := buildEndpoint("http", "seaweedfs.wandb.svc.cluster.local", "80", "bucket", "us-east-1", "clickhouse/", false) + Expect(err).NotTo(HaveOccurred()) + Expect(ep).To(Equal("http://seaweedfs.wandb.svc.cluster.local:80/bucket/clickhouse/")) + }) + + It("defaults to https for an external host with tls enabled", func() { + ep, err := buildEndpoint("", "minio.example.com", "9000", "data", "", "clickhouse/", true) + Expect(err).NotTo(HaveOccurred()) + Expect(ep).To(Equal("https://minio.example.com:9000/data/clickhouse/")) + }) + + It("uses http for an external host when tls is disabled", func() { + ep, err := buildEndpoint("", "minio.example.com", "9000", "data", "", "clickhouse/", false) + Expect(err).NotTo(HaveOccurred()) + Expect(ep).To(Equal("http://minio.example.com:9000/data/clickhouse/")) + }) + + It("derives an AWS virtual-hosted endpoint when no host is set", func() { + ep, err := buildEndpoint("", "", "", "my-bucket", "us-west-2", "clickhouse/", true) + Expect(err).NotTo(HaveOccurred()) + Expect(ep).To(Equal("https://my-bucket.s3.us-west-2.amazonaws.com/clickhouse/")) + }) + + It("errors when neither host nor region is available", func() { + _, err := buildEndpoint("", "", "", "my-bucket", "", "clickhouse/", true) + Expect(err).To(HaveOccurred()) + }) +}) + +var _ = Describe("Object storage prefix", func() { + It("defaults when empty", func() { + Expect(normalizePrefix("")).To(Equal(DefaultObjectStoragePrefix)) + }) + + It("normalizes leading and trailing slashes", func() { + Expect(normalizePrefix("/foo/bar/")).To(Equal("foo/bar/")) + Expect(normalizePrefix("foo")).To(Equal("foo/")) + }) +}) + +var _ = Describe("Storage configuration settings", func() { + It("defines an s3 disk, cache, policy, default routing, and local system logs", func() { + ref := corev1.LocalObjectReference{Name: "objstore-conn"} + oc := &ObjectStorageConn{ + Endpoint: "http://host:80/bucket/clickhouse/", + Region: "us-east-1", + AccessKeyRef: corev1.SecretKeySelector{LocalObjectReference: ref, Key: "AccessKey"}, + SecretKeyRef: corev1.SecretKeySelector{LocalObjectReference: ref, Key: "SecretKey"}, + } + settings := v1.NewSettings() + applyStorageConfiguration(settings, oc, 8<<30) + + Expect(settings.Get("storage_configuration/disks/s3_disk/type").String()).To(Equal("s3")) + Expect(settings.Get("storage_configuration/disks/s3_disk/endpoint").String()).To(Equal("http://host:80/bucket/clickhouse/")) + Expect(settings.Get("storage_configuration/disks/s3_disk/region").String()).To(Equal("us-east-1")) + Expect(settings.Get("storage_configuration/disks/s3_disk_cache/disk").String()).To(Equal("s3_disk")) + Expect(settings.Get("storage_configuration/disks/s3_disk_cache/max_size").String()).To(Equal("8589934592")) + Expect(settings.Get("storage_configuration/policies/s3_main/volumes/main/disk").String()).To(Equal("s3_disk_cache")) + + // s3_main is the server-wide default for all MergeTree tables. + Expect(settings.Get("merge_tree/storage_policy").String()).To(Equal(StoragePolicyName)) + + // Credentials are secret references (operator renders from_env), not literals. + accessKey := settings.Get("storage_configuration/disks/s3_disk/access_key_id") + Expect(accessKey.IsSource()).To(BeTrue()) + Expect(accessKey.GetSecretKeyRef()).NotTo(BeNil()) + Expect(accessKey.GetSecretKeyRef().Name).To(Equal("objstore-conn")) + Expect(accessKey.GetSecretKeyRef().Key).To(Equal("AccessKey")) + Expect(settings.Has("storage_configuration/disks/s3_disk/use_environment_credentials")).To(BeFalse()) + }) + + It("uses ambient credentials when no access keys are present", func() { + oc := &ObjectStorageConn{Endpoint: "https://b.s3.us-east-1.amazonaws.com/clickhouse/", UseEnvCredentials: true} + settings := v1.NewSettings() + applyStorageConfiguration(settings, oc, 1024) + + Expect(settings.Get("storage_configuration/disks/s3_disk/use_environment_credentials").String()).To(Equal("true")) + Expect(settings.Has("storage_configuration/disks/s3_disk/access_key_id")).To(BeFalse()) + Expect(settings.Has("storage_configuration/disks/s3_disk/region")).To(BeFalse()) + }) + + It("renders the s3 disk before the cache disk that wraps it", func() { + oc := &ObjectStorageConn{Endpoint: "http://host:80/bucket/clickhouse/", UseEnvCredentials: true} + settings := v1.NewSettings() + applyStorageConfiguration(settings, oc, 1<<30) + + // ClickHouse initializes disks in document order and requires the wrapped + // disk to be defined before the cache disk; verify the rendered XML order. + rendered := settings.ClickHouseConfig() + diskIdx := strings.Index(rendered, "<"+s3DiskName+">") + cacheIdx := strings.Index(rendered, "<"+s3CacheDiskName+">") + Expect(diskIdx).To(BeNumerically(">=", 0)) + Expect(cacheIdx).To(BeNumerically(">=", 0)) + Expect(diskIdx).To(BeNumerically("<", cacheIdx), "s3 disk must be rendered before the cache disk that wraps it") + }) +}) diff --git a/internal/controller/infra/managed/clickhouse/altinity/purge.go b/internal/controller/infra/managed/clickhouse/altinity/purge.go new file mode 100644 index 00000000..44070b2f --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/purge.go @@ -0,0 +1,66 @@ +package altinity + +import ( + "context" + + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// PurgeFinalizer deletes PVCs belonging to ClickHouse when the retention policy +// is Purge. The Altinity operator normally handles this via PVCReclaimPolicy, +// but we also clean up here as a safety net. +func PurgeFinalizer( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + onDeleteRule common.OnDeleteRule, +) error { + ctx, _ = logx.WithSlog(ctx, logx.ClickHouse) + if onDeleteRule.Policy != common.Purge { + return nil + } + return purgeAssociatedResources(ctx, cl, specNamespacedName.Namespace, onDeleteRule.Selector) +} + +func purgeAssociatedResources( + ctx context.Context, + cl client.Client, + namespace string, + onDeleteSelector labels.Selector, +) error { + log := logx.GetSlog(ctx) + listOptions := &client.ListOptions{ + Namespace: namespace, + LabelSelector: onDeleteSelector, + } + + // PVCs + pvcList := &corev1.PersistentVolumeClaimList{} + if err := cl.List(ctx, pvcList, listOptions); err != nil { + return err + } + if len(pvcList.Items) > 0 { + log.Info( + "Purging associated PVCs", + "count", len(pvcList.Items), "selector", onDeleteSelector.String(), + ) + } else { + log.Debug( + "No associated PVCs found to purge", + "selector", onDeleteSelector.String(), + ) + } + for _, pvc := range pvcList.Items { + if err := cl.Delete(ctx, &pvc); err != nil && !errors.IsNotFound(err) { + return err + } + } + + return nil +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/read.go b/internal/controller/infra/managed/clickhouse/altinity/read.go new file mode 100644 index 00000000..c5c3ac42 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/read.go @@ -0,0 +1,216 @@ +package altinity + +import ( + "context" + "fmt" + "strconv" + + apiv2 "github.com/wandb/operator/api/v2" + ctrlcommon "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + chiv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func readConnectionDetails(actual *chiv1.ClickHouseInstallation) *clickhouseConnInfo { + if actual == nil || actual.Status == nil || actual.Status.Endpoint == "" { + return nil + } + + clickhouseHost := actual.Status.Endpoint + clickhouseHTTPPort := strconv.Itoa(ClickHouseHTTPPort) + clickhouseTCPPort := strconv.Itoa(ClickHouseNativePort) + + return &clickhouseConnInfo{ + Host: clickhouseHost, + HTTPPort: clickhouseHTTPPort, + TCPPort: clickhouseTCPPort, + User: ClickHouseUser, + Password: ClickHousePassword, + Database: ClickHouseDatabase, + } +} + +func ReadState( + ctx context.Context, + k8sClient client.Client, + specNamespacedName types.NamespacedName, + wandbOwner client.Object, + onDeleteRule ctrlcommon.OnDeleteRule, +) ([]metav1.Condition, *apiv2.ClickHouseConnection) { + ctx, log := logx.WithSlog(ctx, logx.ClickHouse) + var actual = &chiv1.ClickHouseInstallation{} + + nsnBuilder := createNsNameBuilder(specNamespacedName) + + found, err := ctrlcommon.GetResource( + ctx, k8sClient, nsnBuilder.InstallationNsName(), ResourceTypeName, actual, + ) + if err != nil { + return []metav1.Condition{ + { + Type: ClickHouseCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + }, nil + } + + conditions := make([]metav1.Condition, 0) + + if !found { + actual = nil + if onDeleteRule.Policy == ctrlcommon.Purge { + log.Debug( + "Attempting to purge associated clickhouse resources after deletion", + "installationName", nsnBuilder.InstallationName(), + ) + if err := purgeAssociatedResources(ctx, k8sClient, specNamespacedName.Namespace, onDeleteRule.Selector); err != nil { + conditions = append(conditions, metav1.Condition{ + Type: ClickHouseCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }) + } else { + conditions = append(conditions, metav1.Condition{ + Type: ClickHouseCustomResourceType, + Status: metav1.ConditionFalse, + Reason: ctrlcommon.PendingDeleteReason, + }) + } + } + } + + var connection *apiv2.ClickHouseConnection + + if actual != nil { + podsRunning, err := chPodsRunningStatus(ctx, k8sClient, nsnBuilder.Namespace(), actual) + if err != nil { + return []metav1.Condition{ + { + Type: ClickHouseReportedReadyType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + }, nil + } + + connInfo := readConnectionDetails(actual) + + connection, err = writeClickHouseConnInfo( + ctx, k8sClient, wandbOwner, nsnBuilder, connInfo, + ) + if err != nil { + if err.Error() == "missing connection info" { + return []metav1.Condition{ + { + Type: ClickHouseConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: ctrlcommon.NoResourceReason, + }, + }, nil + } + return []metav1.Condition{ + { + Type: ClickHouseConnectionInfoType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + }, nil + } + if connection == nil { + conditions = append(conditions, metav1.Condition{ + Type: ClickHouseConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: ctrlcommon.NoResourceReason, + }) + } else { + conditions = append(conditions, metav1.Condition{ + Type: ClickHouseConnectionInfoType, + Status: metav1.ConditionTrue, + Reason: ctrlcommon.ResourceExistsReason, + }) + } + + conditions = append(conditions, computeClickHouseReportedReadyCondition(ctx, actual, podsRunning)...) + } + + return conditions, connection +} + +func chPodsRunningStatus( + ctx context.Context, client client.Client, namespace string, chi *chiv1.ClickHouseInstallation, +) ( + map[string]bool, error, +) { + var result = make(map[string]bool) + + if chi == nil { + return result, nil + } + if chi.Status != nil && chi.Status.Pods != nil { + for _, podName := range chi.Status.Pods { + var pod = &corev1.Pod{} + nsName := types.NamespacedName{Namespace: namespace, Name: podName} + found, err := ctrlcommon.GetResource( + ctx, client, nsName, "ClickhousePod", pod, + ) + if err != nil { + return result, err + } + if found { + result[podName] = ctrlcommon.PodReady(pod) + } else { + result[podName] = false + } + } + } + return result, nil +} + +func computeClickHouseReportedReadyCondition( + ctx context.Context, chi *chiv1.ClickHouseInstallation, podsRunning map[string]bool, +) []metav1.Condition { + log := logx.GetSlog(ctx) + + if chi == nil { + return []metav1.Condition{} + } + + var runningCount, podCount int + for _, isRunning := range podsRunning { + podCount++ + if isRunning { + runningCount++ + } + } + + log.Info( + "Clickhouse pods status", "running", runningCount, "total", podCount, + ) + + status := metav1.ConditionUnknown + reason := ctrlcommon.UnknownReason + message := "" + + if podCount > 0 && podCount == runningCount { + status = metav1.ConditionTrue + reason = ctrlcommon.ResourceExistsReason + } else if podCount > 0 { + status = metav1.ConditionFalse + reason = ctrlcommon.NoResourceReason + message = fmt.Sprintf("%d of %d pods running", runningCount, podCount) + } + + return []metav1.Condition{ + { + Type: ClickHouseReportedReadyType, + Status: status, + Reason: reason, + Message: message, + }, + } +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/spec.go b/internal/controller/infra/managed/clickhouse/altinity/spec.go new file mode 100644 index 00000000..cd8dcca4 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/spec.go @@ -0,0 +1,334 @@ +package altinity + +import ( + "context" + "crypto/sha256" + "fmt" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/managed/clickhouse/altinity/keeper" + "github.com/wandb/operator/internal/logx" + "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + chtypes "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" +) + +const ( + ClickhouseModuleName = "clickhouse" + + // TODO: remove this hardcoded default once all supported manifest versions + // supply clickhouse.<instance>.images.server. + defaultClickHouseImage = "altinity/clickhouse-server:25.8.16.10002.altinitystable" + + objectStoreWaitMaxAttempts = 150 + objectStoreWaitDelaySeconds = 2 +) + +func ClickHouseImage(img manifest.ImageRef, globalImageRegistry string) string { + if out := img.GetImage(globalImageRegistry); out != "" { + return out + } + // Fallback for older manifests that don't supply the image. + return defaultClickHouseImage +} + +const ( + clickHouseRunAsUser int64 = 101 + clickHouseRunAsGroup int64 = 101 + clickHouseFSGroup int64 = 101 + + clickHouseTmpVolumeName = "clickhouse-tmp" + clickHouseTmpMountPath = "/tmp" + clickHouseLogVolumeName = "clickhouse-log" + clickHouseLogMountPath = "/var/log/clickhouse-server" + clickHouseRunVolumeName = "clickhouse-run" + clickHouseRunMountPath = "/var/run/clickhouse-server" + + clickHouseCapabilityAll corev1.Capability = "ALL" +) + +func clickHousePodSecurityContext() *corev1.PodSecurityContext { + if utils.IsOpenShift() { + return &corev1.PodSecurityContext{ + RunAsNonRoot: ptr.To(true), + SeccompProfile: clickHouseRuntimeDefaultSeccompProfile(), + } + } + + return &corev1.PodSecurityContext{ + RunAsUser: ptr.To(clickHouseRunAsUser), + RunAsGroup: ptr.To(clickHouseRunAsGroup), + RunAsNonRoot: ptr.To(true), + FSGroup: ptr.To(clickHouseFSGroup), + SeccompProfile: clickHouseRuntimeDefaultSeccompProfile(), + } +} + +func clickHouseContainerSecurityContext() *corev1.SecurityContext { + securityContext := &corev1.SecurityContext{ + RunAsNonRoot: ptr.To(true), + AllowPrivilegeEscalation: ptr.To(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{clickHouseCapabilityAll}, + }, + SeccompProfile: clickHouseRuntimeDefaultSeccompProfile(), + } + if !utils.IsOpenShift() { + securityContext.RunAsUser = ptr.To(clickHouseRunAsUser) + securityContext.RunAsGroup = ptr.To(clickHouseRunAsGroup) + } + return securityContext +} + +func clickHouseWritableVolumes() []corev1.Volume { + return []corev1.Volume{ + writableEmptyDirVolume(clickHouseTmpVolumeName), + writableEmptyDirVolume(clickHouseLogVolumeName), + writableEmptyDirVolume(clickHouseRunVolumeName), + } +} + +func clickHouseWritableVolumeMounts() []corev1.VolumeMount { + return []corev1.VolumeMount{ + {Name: clickHouseTmpVolumeName, MountPath: clickHouseTmpMountPath}, + {Name: clickHouseLogVolumeName, MountPath: clickHouseLogMountPath}, + {Name: clickHouseRunVolumeName, MountPath: clickHouseRunMountPath}, + } +} + +func clickHouseRuntimeDefaultSeccompProfile() *corev1.SeccompProfile { + return &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault} +} + +func writableEmptyDirVolume(name string) corev1.Volume { + return corev1.Volume{ + Name: name, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + } +} + +// ToClickHouseVendorSpec converts a ClickHouseSpec to a ClickHouseInstallation CR. +// This function translates the high-level ClickHouse spec into the vendor-specific +// ClickHouseInstallation format used by the Altinity operator. +func ToClickHouseVendorSpec( + ctx context.Context, + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedClickHouseSpec, + scheme *runtime.Scheme, + objStorage *ObjectStorageConn, + waitForObjectStore bool, + mfst manifest.Manifest, +) (*v1.ClickHouseInstallation, error) { + _, log := logx.WithSlog(ctx, logx.ClickHouse) + + if spec == nil { + return nil, nil + } + + // Managed ClickHouse stores table data in the object store; require it. + if objStorage == nil { + return nil, fmt.Errorf("managed ClickHouse requires object storage, but none was resolved") + } + + nsnBuilder := CreateNsNameBuilder(types.NamespacedName{ + Namespace: spec.Namespace, Name: spec.Name, + }) + + // This PV holds only metadata/system tables and the S3 read-through cache; + // table data lives in the bucket. + storageQuantity := resource.MustParse(spec.StorageSize) + + // Reserve ~20% of the local PV for metadata/system; the rest backs the cache. + cacheMaxSizeBytes := storageQuantity.Value() * 8 / 10 + + // Create user settings with password + passwordSha256 := fmt.Sprintf("%x", sha256.Sum256([]byte(ClickHousePassword))) + userSettings := v1.NewSettings() + userSettings.Set( + fmt.Sprintf("%s/password_sha256_hex", ClickHouseUser), + v1.NewSettingScalar(passwordSha256), + ) + userSettings.Set( + fmt.Sprintf("%s/networks/ip", ClickHouseUser), + v1.NewSettingScalar("::/0"), + ) + userSettings.Set( + fmt.Sprintf("%s/allow_databases/database", ClickHouseUser), + v1.NewSettingVector([]string{ClickHouseDatabase, "db_management"}), + ) + + // Create server settings + serverSettings := v1.NewSettings() + + // Define the S3 disk + cache + storage policy and make it the server-wide default. + applyStorageConfiguration(serverSettings, objStorage, cacheMaxSizeBytes) + + // Enable built-in Prometheus metrics endpoint if telemetry is enabled + if spec.Telemetry.Enabled { + serverSettings.Set("prometheus/endpoint", v1.NewSettingScalar("/metrics")) + serverSettings.Set("prometheus/port", v1.NewSettingScalar("9363")) + serverSettings.Set("prometheus/metrics", v1.NewSettingScalar("true")) + serverSettings.Set("prometheus/events", v1.NewSettingScalar("true")) + serverSettings.Set("prometheus/asynchronous_metrics", v1.NewSettingScalar("true")) + serverSettings.Set("prometheus/status_info", v1.NewSettingScalar("true")) + } + + reclaimPolicy := v1.PVCReclaimPolicyUnspecified + if wandb.GetRetentionPolicy(spec.ManagedInfraSpec).OnDelete == apiv2.PurgeOnDelete { + reclaimPolicy = v1.PVCReclaimPolicyDelete + } + + clickHouseImage := ClickHouseImage(mfst.Clickhouse["default"].Images["server"], wandb.Spec.Global.ImageRegistry) + podSpec := corev1.PodSpec{ + SecurityContext: clickHousePodSecurityContext(), + Affinity: wandb.GetAffinity(spec.ManagedInfraSpec), + Tolerations: *wandb.GetTolerations(spec.ManagedInfraSpec), + Volumes: clickHouseWritableVolumes(), + Containers: []corev1.Container{ + { + Name: "clickhouse", + Image: clickHouseImage, + SecurityContext: clickHouseContainerSecurityContext(), + VolumeMounts: clickHouseWritableVolumeMounts(), + }, + }, + } + if waitForObjectStore { + podSpec.InitContainers = []corev1.Container{clickHouseObjectStoreWaitContainer(objStorage.Endpoint, clickHouseImage)} + } + + if len(spec.Config.Resources.Requests) > 0 || len(spec.Config.Resources.Limits) > 0 { + podSpec.Containers[0].Resources = corev1.ResourceRequirements{ + Requests: spec.Config.Resources.Requests, + Limits: spec.Config.Resources.Limits, + } + } + + chi := &v1.ClickHouseInstallation{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsnBuilder.InstallationName(), + Namespace: nsnBuilder.Namespace(), + Labels: map[string]string{ + "app": CHIName, + }, + }, + Spec: v1.ChiSpec{ + Configuration: &v1.Configuration{ + Clusters: []*v1.Cluster{ + { + Name: chiClusterName, + Layout: &v1.ChiClusterLayout{ + ShardsCount: ShardsCount, + ReplicasCount: int(spec.Replicas), + }, + }, + }, + Users: userSettings, + Settings: serverSettings, + Zookeeper: &v1.ZookeeperConfig{ + Nodes: v1.ZookeeperNodes{ + { + Host: keeper.ClientServiceFQDN(spec.Namespace, baseName(spec.Name)), + Port: chtypes.NewInt32(int32(keeper.KeeperClientPort)), + }, + }, + }, + }, + Defaults: &v1.Defaults{ + Templates: &v1.TemplatesList{ + PodTemplate: nsnBuilder.PodTemplateName(), + DataVolumeClaimTemplate: nsnBuilder.VolumeTemplateName(), + }, + }, + Templates: &v1.Templates{ + PodTemplates: []v1.PodTemplate{ + { + Name: nsnBuilder.PodTemplateName(), + ObjectMeta: metav1.ObjectMeta{ + Labels: BuildWandbClickhouseLabels(wandb), + }, + Spec: podSpec, + }, + }, + VolumeClaimTemplates: []v1.VolumeClaimTemplate{ + { + Name: nsnBuilder.VolumeTemplateName(), + ObjectMeta: metav1.ObjectMeta{ + Labels: BuildWandbClickhouseLabels(wandb), + }, + StorageManagement: v1.StorageManagement{ + PVCReclaimPolicy: reclaimPolicy, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: storageQuantity, + }, + }, + }, + }, + }, + }, + }, + } + + // Set owner reference + if err := ctrl.SetControllerReference(wandb, chi, scheme); err != nil { + log.Error("failed to set owner reference on CHI CR", logx.ErrAttr(err)) + return nil, fmt.Errorf("failed to set owner reference: %w", err) + } + + return chi, nil +} + +func clickHouseObjectStoreWaitContainer(endpoint, image string) corev1.Container { + // The existing ClickHouse image includes wget. Any HTTP response below 500 + // proves DNS and the S3 API are reachable; authentication remains ClickHouse's + // responsibility when its main process starts. + script := fmt.Sprintf( + `attempt=1 +while [ "$attempt" -le %d ]; do + response="$(wget --no-check-certificate --server-response --spider "$1" 2>&1)" + result=$? + if [ "$result" -eq 0 ] || printf '%%s\n' "$response" | grep -Eq 'HTTP/[0-9.]+ [1-4][0-9][0-9]'; then + exit 0 + fi + attempt=$((attempt + 1)) + if [ "$attempt" -le %d ]; then sleep %d; fi +done +echo 'object-store endpoint did not become reachable before timeout' >&2 +exit 1`, + objectStoreWaitMaxAttempts, + objectStoreWaitMaxAttempts, + objectStoreWaitDelaySeconds, + ) + return corev1.Container{ + Name: "wait-object-store", + Image: image, + Command: []string{"/bin/sh", "-c"}, + Args: []string{script, "wait-object-store", endpoint}, + SecurityContext: clickHouseContainerSecurityContext(), + } +} + +func BuildWandbClickhouseLabels(wandb *apiv2.WeightsAndBiases) map[string]string { + return common.BuildWandbLabels(wandb, ClickhouseModuleName) +} + +func ToClickHouseOnDeleteRule(wandb *apiv2.WeightsAndBiases, retentionPolicy apiv2.RetentionPolicy) common.OnDeleteRule { + return common.ToOnDeleteRule(wandb, retentionPolicy, ClickhouseModuleName) +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/spec_test.go b/internal/controller/infra/managed/clickhouse/altinity/spec_test.go new file mode 100644 index 00000000..5788ce4f --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/spec_test.go @@ -0,0 +1,242 @@ +package altinity + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/infra/managed/clickhouse/altinity/keeper" + "github.com/wandb/operator/pkg/utils" + chiv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ = Describe("ClickHouse vendor specs", func() { + BeforeEach(func() { + utils.SetOpenShiftMode(false) + }) + + It("renders hardened pod templates with writable runtime mounts", func() { + wandb := clickHouseWandb() + + chi, err := ToClickHouseVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, clickHouseScheme(), testObjectStorageConn(), true, manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(chi).NotTo(BeNil()) + Expect(chi.Spec.Templates.PodTemplates).To(HaveLen(1)) + + podSpec := chi.Spec.Templates.PodTemplates[0].Spec + expectClickHouseDefaultPodSecurityContext(podSpec.SecurityContext) + expectClickHouseWritableVolume(podSpec.Volumes, clickHouseTmpVolumeName) + expectClickHouseWritableVolume(podSpec.Volumes, clickHouseLogVolumeName) + expectClickHouseWritableVolume(podSpec.Volumes, clickHouseRunVolumeName) + + Expect(podSpec.Containers).To(HaveLen(1)) + Expect(podSpec.InitContainers).To(HaveLen(1)) + wait := podSpec.InitContainers[0] + Expect(wait.Name).To(Equal("wait-object-store")) + Expect(wait.Image).To(Equal(podSpec.Containers[0].Image)) + Expect(wait.Args).To(HaveLen(3)) + Expect(wait.Args[0]).To(ContainSubstring("wget")) + Expect(wait.Args[0]).To(ContainSubstring("HTTP/[0-9.]+ [1-4]")) + Expect(wait.Args[0]).To(ContainSubstring("sleep 2")) + Expect(wait.Args[0]).NotTo(ContainSubstring("aws")) + Expect(wait.Args[0]).NotTo(ContainSubstring("head-bucket")) + Expect(wait.Args[0]).NotTo(ContainSubstring("AccessKey")) + Expect(wait.Args[0]).NotTo(ContainSubstring("SecretKey")) + Expect(wait.Args[2]).To(Equal(testObjectStorageConn().Endpoint)) + Expect(wait.Env).To(BeEmpty()) + container := podSpec.Containers[0] + Expect(container.Image).To(Equal(ClickHouseImage(manifest.ImageRef{}, ""))) + Expect(container.Resources.Requests[corev1.ResourceCPU]).To(Equal(resource.MustParse("500m"))) + expectClickHouseDefaultContainerSecurityContext(container.SecurityContext) + expectClickHouseWritableMount(container.VolumeMounts, clickHouseTmpVolumeName, clickHouseTmpMountPath) + expectClickHouseWritableMount(container.VolumeMounts, clickHouseLogVolumeName, clickHouseLogMountPath) + expectClickHouseWritableMount(container.VolumeMounts, clickHouseRunVolumeName, clickHouseRunMountPath) + }) + + It("omits fixed ClickHouse IDs in OpenShift mode", func() { + utils.SetOpenShiftMode(true) + + wandb := clickHouseWandb() + chi, err := ToClickHouseVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, clickHouseScheme(), testObjectStorageConn(), true, manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(chi).NotTo(BeNil()) + + podSpec := chi.Spec.Templates.PodTemplates[0].Spec + expectClickHouseOpenShiftPodSecurityContext(podSpec.SecurityContext) + expectClickHouseOpenShiftContainerSecurityContext(podSpec.Containers[0].SecurityContext) + }) + + It("backs storage with the object store, sets a default policy, and wires keeper", func() { + wandb := clickHouseWandb() + chi, err := ToClickHouseVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, clickHouseScheme(), testObjectStorageConn(), true, manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(chi).NotTo(BeNil()) + + settings := chi.Spec.Configuration.Settings + + // Server-wide default storage policy routes all MergeTree tables to the bucket. + Expect(settings.Get("merge_tree/storage_policy").String()).To(Equal(StoragePolicyName)) + + // storage_configuration is expressed through the typed Settings API (no XML). + Expect(settings.Get("storage_configuration/disks/s3_disk/type").String()).To(Equal("s3")) + Expect(settings.Get("storage_configuration/disks/s3_disk/endpoint").String()). + To(Equal("http://seaweedfs.wandb.svc.cluster.local:80/bucket/clickhouse/")) + Expect(settings.Get("storage_configuration/policies/" + StoragePolicyName + "/volumes/main/disk").String()). + To(Equal("s3_disk_cache")) + + // Credentials are secret references; the operator renders from_env. No + // config file and no manually-injected env vars on our side. + accessKey := settings.Get("storage_configuration/disks/s3_disk/access_key_id") + Expect(accessKey.IsSource()).To(BeTrue()) + Expect(accessKey.GetSecretKeyRef()).NotTo(BeNil()) + Expect(accessKey.GetSecretKeyRef().Key).To(Equal("AccessKey")) + Expect(chi.Spec.Configuration.Files).To(BeNil()) + Expect(chi.Spec.Templates.PodTemplates[0].Spec.Containers[0].Env).To(BeEmpty()) + + // Keeper wired via the zookeeper config. + Expect(chi.Spec.Configuration.Zookeeper).NotTo(BeNil()) + Expect(chi.Spec.Configuration.Zookeeper.Nodes).To(HaveLen(1)) + Expect(chi.Spec.Configuration.Zookeeper.Nodes[0].Host).To(Equal(keeper.ClientServiceFQDN("wandb", "clickhouse"))) + }) + + It("does not gate ClickHouse for bring-your-own object storage", func() { + wandb := clickHouseWandb() + chi, err := ToClickHouseVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, clickHouseScheme(), testObjectStorageConn(), false, manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(chi.Spec.Templates.PodTemplates[0].Spec.InitContainers).To(BeEmpty()) + }) +}) + +func testObjectStorageConn() *ObjectStorageConn { + ref := corev1.LocalObjectReference{Name: "objstore-conn"} + return &ObjectStorageConn{ + Endpoint: "http://seaweedfs.wandb.svc.cluster.local:80/bucket/clickhouse/", + Region: "us-east-1", + AccessKeyRef: corev1.SecretKeySelector{LocalObjectReference: ref, Key: "AccessKey"}, + SecretKeyRef: corev1.SecretKeySelector{LocalObjectReference: ref, Key: "SecretKey"}, + } +} + +func clickHouseScheme() *runtime.Scheme { + scheme := runtime.NewScheme() + Expect(apiv2.AddToScheme(scheme)).To(Succeed()) + Expect(chiv1.AddToScheme(scheme)).To(Succeed()) + return scheme +} + +func clickHouseWandb() *apiv2.WeightsAndBiases { + tolerations := []corev1.Toleration{} + return &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiv2.GroupVersion.String(), + Kind: "WeightsAndBiases", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "wandb", + }, + Spec: apiv2.WeightsAndBiasesSpec{ + Tolerations: &tolerations, + ClickHouse: map[string]apiv2.ClickHouseSpec{ + apiv2.DefaultInstanceName: { + ManagedClickHouse: &apiv2.ManagedClickHouseSpec{ + Name: "clickhouse", + Namespace: "wandb", + Replicas: 1, + StorageSize: "10Gi", + Config: apiv2.ClickHouseConfig{ + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + }, + }, + }, + }, + }, + }, + }, + } +} + +func expectClickHouseDefaultPodSecurityContext(securityContext *corev1.PodSecurityContext) { + Expect(securityContext).NotTo(BeNil()) + Expect(securityContext.RunAsUser).NotTo(BeNil()) + Expect(*securityContext.RunAsUser).To(Equal(clickHouseRunAsUser)) + Expect(securityContext.RunAsGroup).NotTo(BeNil()) + Expect(*securityContext.RunAsGroup).To(Equal(clickHouseRunAsGroup)) + Expect(securityContext.RunAsNonRoot).NotTo(BeNil()) + Expect(*securityContext.RunAsNonRoot).To(BeTrue()) + Expect(securityContext.FSGroup).NotTo(BeNil()) + Expect(*securityContext.FSGroup).To(Equal(clickHouseFSGroup)) + Expect(securityContext.SeccompProfile).NotTo(BeNil()) + Expect(securityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) +} + +func expectClickHouseDefaultContainerSecurityContext(securityContext *corev1.SecurityContext) { + Expect(securityContext).NotTo(BeNil()) + Expect(securityContext.RunAsUser).NotTo(BeNil()) + Expect(*securityContext.RunAsUser).To(Equal(clickHouseRunAsUser)) + Expect(securityContext.RunAsGroup).NotTo(BeNil()) + Expect(*securityContext.RunAsGroup).To(Equal(clickHouseRunAsGroup)) + Expect(securityContext.RunAsNonRoot).NotTo(BeNil()) + Expect(*securityContext.RunAsNonRoot).To(BeTrue()) + Expect(securityContext.AllowPrivilegeEscalation).NotTo(BeNil()) + Expect(*securityContext.AllowPrivilegeEscalation).To(BeFalse()) + Expect(securityContext.Capabilities).NotTo(BeNil()) + Expect(securityContext.Capabilities.Drop).To(ContainElement(clickHouseCapabilityAll)) + Expect(securityContext.SeccompProfile).NotTo(BeNil()) + Expect(securityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) +} + +func expectClickHouseWritableMount(mounts []corev1.VolumeMount, name, mountPath string) { + found := false + for _, mount := range mounts { + if mount.Name == name && mount.MountPath == mountPath { + found = true + break + } + } + Expect(found).To(BeTrue()) +} + +func expectClickHouseOpenShiftPodSecurityContext(securityContext *corev1.PodSecurityContext) { + Expect(securityContext).NotTo(BeNil()) + Expect(securityContext.RunAsUser).To(BeNil()) + Expect(securityContext.RunAsGroup).To(BeNil()) + Expect(securityContext.FSGroup).To(BeNil()) + Expect(securityContext.RunAsNonRoot).NotTo(BeNil()) + Expect(*securityContext.RunAsNonRoot).To(BeTrue()) + Expect(securityContext.SeccompProfile).NotTo(BeNil()) + Expect(securityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) +} + +func expectClickHouseOpenShiftContainerSecurityContext(securityContext *corev1.SecurityContext) { + Expect(securityContext).NotTo(BeNil()) + Expect(securityContext.RunAsUser).To(BeNil()) + Expect(securityContext.RunAsGroup).To(BeNil()) + Expect(securityContext.RunAsNonRoot).NotTo(BeNil()) + Expect(*securityContext.RunAsNonRoot).To(BeTrue()) + Expect(securityContext.AllowPrivilegeEscalation).NotTo(BeNil()) + Expect(*securityContext.AllowPrivilegeEscalation).To(BeFalse()) + Expect(securityContext.Capabilities).NotTo(BeNil()) + Expect(securityContext.Capabilities.Drop).To(ContainElement(clickHouseCapabilityAll)) + Expect(securityContext.SeccompProfile).NotTo(BeNil()) + Expect(securityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) +} + +func expectClickHouseWritableVolume(volumes []corev1.Volume, name string) { + found := false + for _, volume := range volumes { + if volume.Name == name && volume.EmptyDir != nil { + found = true + break + } + } + Expect(found).To(BeTrue()) +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/status.go b/internal/controller/infra/managed/clickhouse/altinity/status.go new file mode 100644 index 00000000..244521d1 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/status.go @@ -0,0 +1,245 @@ +package altinity + +import ( + "context" + "time" + + "github.com/samber/lo" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/managed/clickhouse/altinity/keeper" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" +) + +const ( + ClickHouseCustomResourceType = "ClickHouseCustomResource" + ClickHouseConnectionInfoType = "ClickHouseConnectionInfo" + ClickHouseReportedReadyType = "ClickHouseReportedReady" +) + +func ComputeStatus( + ctx context.Context, + enabled bool, + oldConditions, currentConditions []metav1.Condition, + connection *apiv2.ClickHouseConnection, + currentGeneration int64, +) (apiv2.ClickHouseInfraStatus, []corev1.Event, ctrl.Result) { + ctx, _ = logx.WithSlog(ctx, logx.ClickHouse) + result := apiv2.ClickHouseInfraStatus{} + + if connection != nil { + result.Connection = *connection + } + + currentConditions = applyDefaultConditions(currentConditions) + + result.Conditions = common.ComputeConditionUpdates( + oldConditions, + currentConditions, + currentGeneration, + common.DefaultConditionExpiry, + ) + + state, events := inferInfraState(ctx, enabled, result.Conditions) + result.State = state + + result.Ready = !lo.Contains(common.NotReadyStates, result.State) + + requeueAfter := 3 * time.Minute + switch result.State { + case common.ErrorState: + requeueAfter = 15 * time.Second + case common.DegradedState: + requeueAfter = 5 * time.Minute + case common.PendingState: + requeueAfter = 2 * time.Minute + case common.HealthyState: + requeueAfter = 10 * time.Minute + } + + return result, events, ctrl.Result{RequeueAfter: requeueAfter} +} + +func applyDefaultConditions(conditions []metav1.Condition) []metav1.Condition { + if !common.ContainsType(conditions, ClickHouseConnectionInfoType) { + conditions = append(conditions, metav1.Condition{ + Type: ClickHouseConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }) + } + + return conditions +} + +func inferInfraState( + ctx context.Context, + enabled bool, + conditions []metav1.Condition, +) (string, []corev1.Event) { + if !enabled { + return common.UnavailableState, nil + } + var events []corev1.Event + impliedStates := make(map[string]string, len(conditions)) + + // undeployable name = terminal config error; surface it in `kubectl describe` + if cond, found := lo.Find(conditions, func(c metav1.Condition) bool { + return c.Type == ClickHouseCustomResourceType && c.Reason == common.InvalidNameReason + }); found { + events = append(events, corev1.Event{ + Type: corev1.EventTypeWarning, + Reason: "ClickHouseInvalidName", + Message: cond.Message, + }) + } + + impliedStates = inferStateFromCondition(ctx, ClickHouseCustomResourceType, impliedStates, conditions) + impliedStates = inferStateFromCondition(ctx, ClickHouseConnectionInfoType, impliedStates, conditions) + impliedStates = inferStateFromCondition(ctx, ClickHouseReportedReadyType, impliedStates, conditions) + impliedStates = inferStateFromCondition(ctx, keeper.KeeperReportedReadyType, impliedStates, conditions) + + hasImpliedState := func(target string) bool { + return len(lo.FilterValues( + impliedStates, + func(_ string, value string) bool { + return value == target + })) > 0 + } + + summaryState := common.UnknownState + + // if the service is reporting as healthy but the connection info is unavailable + // log the missing connection as an event and mark the infraStatus as 'error' + if impliedStates[ClickHouseConnectionInfoType] == common.UnavailableState && + impliedStates[ClickHouseReportedReadyType] == common.HealthyState { + events = append(events, corev1.Event{ + Type: corev1.EventTypeWarning, + Reason: "ClickHouseConnectionInfoUnavailable", + Message: "ClickHouse connection info is unavailable, but ClickHouse is reported as ready.", + }) + summaryState = common.ErrorState + } + + // if there is a specific state identified, use that + if summaryState != common.UnknownState { + return summaryState, events + } + + // otherwise, return the most significant state mapped for any condition + stateSignificanceOrder := []string{ + common.ErrorState, + common.UnavailableState, + common.PendingState, + common.DegradedState, + common.HealthyState, + } + for _, s := range stateSignificanceOrder { + if hasImpliedState(s) { + return s, events + } + } + + return common.UnknownState, events +} + +func inferStateFromCondition(ctx context.Context, conditionType string, impliedStates map[string]string, conditions []metav1.Condition) map[string]string { + cond, found := lo.Find(conditions, func(c metav1.Condition) bool { return c.Type == conditionType }) + if !found { + impliedStates[conditionType] = common.UnknownState + } else { + switch conditionType { + case ClickHouseCustomResourceType: + impliedStates[conditionType] = inferState_ClickHouseCustomResourceType(ctx, cond) + case ClickHouseConnectionInfoType: + impliedStates[conditionType] = inferState_ClickHouseConnectionInfoType(ctx, cond) + case ClickHouseReportedReadyType: + impliedStates[conditionType] = inferState_ClickHouseReportedReadyType(ctx, cond) + case keeper.KeeperReportedReadyType: + impliedStates[conditionType] = inferState_KeeperReportedReadyType(ctx, cond) + default: + impliedStates[conditionType] = common.UnknownState + } + } + return impliedStates +} + +func inferState_ClickHouseCustomResourceType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + if condition.Reason == common.PendingCreateReason { + result = common.PendingState + } + if condition.Reason == common.PendingDeleteReason { + result = common.UnavailableState + } + if condition.Reason == common.InvalidNameReason { + result = common.ErrorState + } + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} + +func inferState_ClickHouseConnectionInfoType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + result = common.UnavailableState + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} + +func inferState_KeeperReportedReadyType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + // Keeper not ready yet: ClickHouse can't coordinate replication, so hold at pending. + result = common.PendingState + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} + +func inferState_ClickHouseReportedReadyType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + if condition.Reason == common.ResourceErrorReason { + result = common.ErrorState + } else { + result = common.DegradedState + } + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/status_test.go b/internal/controller/infra/managed/clickhouse/altinity/status_test.go new file mode 100644 index 00000000..c3e8e11e --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/status_test.go @@ -0,0 +1,58 @@ +package altinity + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/managed/clickhouse/altinity/keeper" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("ClickHouse status keeper gating", func() { + healthyClickHouse := func() []metav1.Condition { + return []metav1.Condition{ + {Type: ClickHouseCustomResourceType, Status: metav1.ConditionTrue, Reason: common.ResourceExistsReason}, + {Type: ClickHouseConnectionInfoType, Status: metav1.ConditionTrue, Reason: common.ResourceExistsReason}, + {Type: ClickHouseReportedReadyType, Status: metav1.ConditionTrue, Reason: common.ResourceExistsReason}, + } + } + + It("holds at pending while keeper is not ready, even if ClickHouse is healthy", func() { + conditions := append(healthyClickHouse(), metav1.Condition{ + Type: keeper.KeeperReportedReadyType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }) + state, _ := inferInfraState(context.Background(), true, conditions) + Expect(state).To(Equal(common.PendingState)) + }) + + It("is healthy when both keeper and ClickHouse are ready", func() { + conditions := append(healthyClickHouse(), metav1.Condition{ + Type: keeper.KeeperReportedReadyType, + Status: metav1.ConditionTrue, + Reason: common.ResourceExistsReason, + }) + state, _ := inferInfraState(context.Background(), true, conditions) + Expect(state).To(Equal(common.HealthyState)) + }) + + It("reports an error state and event when the managed name cannot be deployed", func() { + conditions := []metav1.Condition{ + { + Type: ClickHouseCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.InvalidNameReason, + Message: "managed ClickHouse name is too long", + }, + } + state, events := inferInfraState(context.Background(), true, conditions) + + Expect(state).To(Equal(common.ErrorState)) + Expect(events).To(HaveLen(1)) + Expect(events[0].Reason).To(Equal("ClickHouseInvalidName")) + Expect(events[0].Message).To(ContainSubstring("too long")) + }) +}) diff --git a/internal/controller/infra/managed/clickhouse/altinity/values.go b/internal/controller/infra/managed/clickhouse/altinity/values.go new file mode 100644 index 00000000..333b07e3 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/values.go @@ -0,0 +1,34 @@ +package altinity + +import ( + "k8s.io/apimachinery/pkg/types" +) + +const ( + // Resource names + CHIName = "wandb-clickhouse" + ServiceName = "clickhouse-wandb-clickhouse" + ConnectionName = "wandb-clickhouse-connection" + + // ClickHouse configuration + ClickHouseNativePort = 9000 + ClickHouseHTTPPort = 8123 + ClickHouseUser = "test_user" + ClickHousePassword = "test_password" + ClickHouseDatabase = "default" + + // Cluster configuration + ShardsCount = 1 +) + +func InstallationName(specName string) string { + return createNsNameBuilder(types.NamespacedName{Name: specName}).InstallationName() +} + +func ClusterName(specName string) string { + return createNsNameBuilder(types.NamespacedName{Name: specName}).ClusterName() +} + +func VolumeTemplateName(specName string) string { + return createNsNameBuilder(types.NamespacedName{Name: specName}).VolumeTemplateName() +} diff --git a/internal/controller/infra/managed/clickhouse/altinity/write.go b/internal/controller/infra/managed/clickhouse/altinity/write.go new file mode 100644 index 00000000..1d0ceb65 --- /dev/null +++ b/internal/controller/infra/managed/clickhouse/altinity/write.go @@ -0,0 +1,107 @@ +package altinity + +import ( + "context" + + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/managed/clickhouse/altinity/keeper" + "github.com/wandb/operator/internal/logx" + chkv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1" + chiv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + ResourceTypeName = "ClickHouseInstallation" + AppConnTypeName = "ClickHouseAppConn" +) + +// WriteState reconciles the Keeper ensemble (first, since ReplicatedMergeTree +// depends on it) and the ClickHouse installation. +func WriteState( + ctx context.Context, + client client.Client, + specNamespacedName types.NamespacedName, + desiredKeeper *chkv1.ClickHouseKeeperInstallation, + desired *chiv1.ClickHouseInstallation, +) []metav1.Condition { + ctx, _ = logx.WithSlog(ctx, logx.ClickHouse) + results := make([]metav1.Condition, 0) + + results = append(results, keeper.WriteState( + ctx, client, + types.NamespacedName{Namespace: desiredKeeper.Namespace, Name: desiredKeeper.Name}, + desiredKeeper, + )...) + results = append(results, writeClickHouseInstallation(ctx, client, specNamespacedName, desired)...) + + return results +} + +// writeClickHouseInstallation create-or-updates the CHI, setting only the fields +// we own (spec, labels, owner refs) and preserving the Altinity-managed +// finalizer/status. It compares owned fields via JSON, never the vendored status +// — whose uint64 and unexported fields panic controllerutil's reflective +// diff/copy. +func writeClickHouseInstallation( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + desired *chiv1.ClickHouseInstallation, +) []metav1.Condition { + nsnBuilder := createNsNameBuilder(specNamespacedName) + obj := &chiv1.ClickHouseInstallation{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsnBuilder.InstallationName(), + Namespace: nsnBuilder.Namespace(), + }, + } + + op, err := common.WriteOwnedFields(ctx, cl, obj, + func(o *chiv1.ClickHouseInstallation) { + applyOwnedMetadata(o, desired) + o.Spec = desired.Spec + }, + clickHouseOwnedEqual, + ) + if err != nil { + return []metav1.Condition{ + {Type: common.ReconciledType, Status: metav1.ConditionFalse, Reason: common.ApiErrorReason}, + {Type: ClickHouseCustomResourceType, Status: metav1.ConditionUnknown, Reason: common.ApiErrorReason}, + } + } + + return []metav1.Condition{customResourceConditionForOp(ClickHouseCustomResourceType, op)} +} + +func clickHouseOwnedEqual(a, b *chiv1.ClickHouseInstallation) bool { + return common.JSONEqual(a.Spec, b.Spec) && + common.JSONEqual(a.Labels, b.Labels) && + common.JSONEqual(a.OwnerReferences, b.OwnerReferences) +} + +// applyOwnedMetadata sets the metadata we own (merged labels, owner references), +// leaving finalizers/annotations owned by the resource's operator untouched. +func applyOwnedMetadata(obj, desired metav1.Object) { + labels := obj.GetLabels() + if labels == nil { + labels = map[string]string{} + } + for k, v := range desired.GetLabels() { + labels[k] = v + } + obj.SetLabels(labels) + obj.SetOwnerReferences(desired.GetOwnerReferences()) +} + +// customResourceConditionForOp maps a create-or-update result to the resource's +// existence condition (created => pending, updated/unchanged => exists). +func customResourceConditionForOp(conditionType string, op controllerutil.OperationResult) metav1.Condition { + if op == controllerutil.OperationResultCreated { + return metav1.Condition{Type: conditionType, Status: metav1.ConditionFalse, Reason: common.PendingCreateReason} + } + return metav1.Condition{Type: conditionType, Status: metav1.ConditionTrue, Reason: common.ResourceExistsReason} +} diff --git a/internal/controller/infra/managed/kafka/bufstream/config.go b/internal/controller/infra/managed/kafka/bufstream/config.go new file mode 100644 index 00000000..b4c30f4e --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/config.go @@ -0,0 +1,181 @@ +package bufstream + +import ( + "fmt" + "strings" + + "gopkg.in/yaml.v3" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/infra/external/objectstore" +) + +type dataSource struct { + EnvVar string `yaml:"env_var,omitempty"` +} + +type bufstreamListener struct { + Name string `yaml:"name"` + ListenAddress string `yaml:"listen_address"` + AdvertiseAddress string `yaml:"advertise_address,omitempty"` +} + +type bufstreamKafka struct { + Listeners []bufstreamListener `yaml:"listeners"` +} + +type bufstreamS3 struct { + URI string `yaml:"uri"` + Region string `yaml:"region,omitempty"` + Endpoint string `yaml:"endpoint,omitempty"` + AccessKeyID *dataSource `yaml:"access_key_id,omitempty"` + SecretAccessKey *dataSource `yaml:"secret_access_key,omitempty"` + ForcePathStyle bool `yaml:"force_path_style"` +} + +type bufstreamAzure struct { + URI string `yaml:"uri"` + AccessKeyID *dataSource `yaml:"access_key_id,omitempty"` + SecretAccessKey *dataSource `yaml:"secret_access_key,omitempty"` +} + +type bufstreamData struct { + S3 *bufstreamS3 `yaml:"s3,omitempty"` + GCS string `yaml:"gcs,omitempty"` + Azure *bufstreamAzure `yaml:"azure,omitempty"` +} + +type bufstreamEtcd struct { + Addresses []string `yaml:"addresses"` +} + +type bufstreamMetadata struct { + Etcd bufstreamEtcd `yaml:"etcd"` +} + +type bufstreamListenAddr struct { + ListenAddress string `yaml:"listen_address"` +} + +type bufstreamConfig struct { + Version string `yaml:"version"` + Cluster string `yaml:"cluster"` + Kafka bufstreamKafka `yaml:"kafka"` + Data bufstreamData `yaml:"data"` + Metadata bufstreamMetadata `yaml:"metadata"` + Debug bufstreamListenAddr `yaml:"debug"` + Admin bufstreamListenAddr `yaml:"admin"` +} + +// renderBufstreamConfig produces the bufstream.yaml contents for a broker that +// stores data in the configured object-store provider and metadata in the given +// etcd cluster endpoints. +func renderBufstreamConfig(clusterName, advertiseHost string, etcdAddresses []string, storage objectstore.ConnInfo) (string, error) { + // Isolate Bufstream's objects under a dedicated key prefix (the cluster name) + // so they never collide with W&B artifact data, which shares the same bucket. + // storage is passed by value, so this only affects the rendered config. + uri := "" + switch storage.Provider { + case apiv2.ObjectStoreProviderS3: + uri = fmt.Sprintf("s3://%s", storage.Bucket) + case apiv2.ObjectStoreProviderGCS: + uri = fmt.Sprintf("gs://%s", storage.Bucket) + case apiv2.ObjectStoreProviderAzure: + // For Azure the connection's AccessKey carries the storage account. + uri = fmt.Sprintf("https://%s.blob.core.windows.net/%s", storage.AccessKey, storage.Bucket) + } + + if storage.Path != "" { + uri = fmt.Sprintf("%s/%s", uri, storage.Path) + } + + uri = fmt.Sprintf("%s/%s", uri, clusterName) + + data, err := renderData(storage, uri) + if err != nil { + return "", err + } + + cfg := bufstreamConfig{ + Version: "v1beta1", + Cluster: clusterName, + Kafka: bufstreamKafka{ + Listeners: []bufstreamListener{ + { + Name: "external", + ListenAddress: fmt.Sprintf("0.0.0.0:%d", KafkaListenerPort), + AdvertiseAddress: fmt.Sprintf("%s:%d", advertiseHost, KafkaListenerPort), + }, + }, + }, + Data: data, + Metadata: bufstreamMetadata{ + Etcd: bufstreamEtcd{Addresses: etcdAddresses}, + }, + Debug: bufstreamListenAddr{ListenAddress: fmt.Sprintf("0.0.0.0:%d", DebugPort)}, + Admin: bufstreamListenAddr{ListenAddress: fmt.Sprintf("0.0.0.0:%d", AdminPort)}, + } + + out, err := yaml.Marshal(&cfg) + if err != nil { + return "", fmt.Errorf("failed to marshal bufstream config: %w", err) + } + return string(out), nil +} + +// renderData maps the resolved object-store connection onto Bufstream's +// provider-specific data storage config. +func renderData(storage objectstore.ConnInfo, uri string) (bufstreamData, error) { + switch storage.Provider { + case apiv2.ObjectStoreProviderS3: + return bufstreamData{S3: renderS3Storage(storage, uri)}, nil + case apiv2.ObjectStoreProviderGCS: + // GCS authenticates via workload identity / ADC, so only the bucket URI is configured. + return bufstreamData{GCS: uri}, nil + case apiv2.ObjectStoreProviderAzure: + return bufstreamData{Azure: renderAzureStorage(storage, uri)}, nil + default: + return bufstreamData{}, fmt.Errorf("unsupported object-store provider %q", storage.Provider) + } +} + +func renderS3Storage(storage objectstore.ConnInfo, uri string) *bufstreamS3 { + region := storage.Region + if region == "" { + region = "us-east-1" + } + + endpoint := "" + if storage.Endpoint != "" { + if strings.Contains(storage.Endpoint, "://") { + endpoint = storage.Endpoint + } else { + scheme := "http" + if storage.TlsEnabled { + scheme = "https" + } + endpoint = fmt.Sprintf("%s://%s:%s", scheme, storage.Endpoint, storage.Port) + + } + } + s3 := &bufstreamS3{ + URI: uri, + Region: region, + Endpoint: endpoint, + ForcePathStyle: storage.ForcePathStyle, + } + if storage.HasStaticCredentials() { + s3.AccessKeyID = &dataSource{EnvVar: EnvStorageAccessKeyID} + s3.SecretAccessKey = &dataSource{EnvVar: EnvStorageSecretAccessKey} + } + return s3 +} + +func renderAzureStorage(storage objectstore.ConnInfo, uri string) *bufstreamAzure { + az := &bufstreamAzure{URI: uri} + if storage.HasStaticCredentials() { + az.AccessKeyID = &dataSource{EnvVar: EnvStorageAccessKeyID} + az.SecretAccessKey = &dataSource{EnvVar: EnvStorageSecretAccessKey} + } + return az +} diff --git a/internal/controller/infra/managed/kafka/bufstream/config_test.go b/internal/controller/infra/managed/kafka/bufstream/config_test.go new file mode 100644 index 00000000..4211a066 --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/config_test.go @@ -0,0 +1,112 @@ +package bufstream + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/infra/external/objectstore" +) + +func TestRenderBufstreamConfigS3(t *testing.T) { + storage := objectstore.ConnInfo{ + Provider: apiv2.ObjectStoreProviderS3, + URI: "s3://wandb-bucket", + Bucket: "wandb-bucket", + Endpoint: "http://objstore.default.svc.cluster.local:8333", + Region: "us-east-1", + AccessKey: "ak", + SecretKey: "sk", + ForcePathStyle: true, + } + + etcdAddrs := []string{ + "my-kafka-etcd-0.my-kafka-etcd.default.svc.cluster.local:2379", + "my-kafka-etcd-1.my-kafka-etcd.default.svc.cluster.local:2379", + "my-kafka-etcd-2.my-kafka-etcd.default.svc.cluster.local:2379", + } + rendered, err := renderBufstreamConfig("my-kafka", "my-kafka.default.svc.cluster.local", etcdAddrs, storage) + require.NoError(t, err) + + // Round-trips as valid YAML. + var parsed bufstreamConfig + require.NoError(t, yaml.Unmarshal([]byte(rendered), &parsed)) + + require.Equal(t, "v1beta1", parsed.Version) + require.Equal(t, "my-kafka", parsed.Cluster) + require.Len(t, parsed.Kafka.Listeners, 1) + require.Equal(t, "0.0.0.0:9092", parsed.Kafka.Listeners[0].ListenAddress) + require.Equal(t, "my-kafka.default.svc.cluster.local:9092", parsed.Kafka.Listeners[0].AdvertiseAddress) + + require.NotNil(t, parsed.Data.S3) + // Bufstream data is isolated under a per-cluster key prefix. + require.Equal(t, "s3://wandb-bucket/my-kafka", parsed.Data.S3.URI) + require.Equal(t, "us-east-1", parsed.Data.S3.Region) + require.Equal(t, "http://objstore.default.svc.cluster.local:8333", parsed.Data.S3.Endpoint) + require.True(t, parsed.Data.S3.ForcePathStyle) + // Credentials are referenced indirectly via env vars, never inlined. + require.NotNil(t, parsed.Data.S3.AccessKeyID) + require.Equal(t, EnvStorageAccessKeyID, parsed.Data.S3.AccessKeyID.EnvVar) + require.Equal(t, EnvStorageSecretAccessKey, parsed.Data.S3.SecretAccessKey.EnvVar) + require.NotContains(t, rendered, "sk") + + require.Equal(t, etcdAddrs, parsed.Metadata.Etcd.Addresses) + require.True(t, strings.Contains(rendered, "0.0.0.0:9090")) +} + +func TestRenderBufstreamConfigS3NoStaticCreds(t *testing.T) { + storage := objectstore.ConnInfo{ + Provider: apiv2.ObjectStoreProviderS3, + URI: "s3://wandb-bucket", + Bucket: "wandb-bucket", + Region: "us-west-2", + } + rendered, err := renderBufstreamConfig("k", "k.ns.svc", []string{"e:2379"}, storage) + require.NoError(t, err) + + var parsed bufstreamConfig + require.NoError(t, yaml.Unmarshal([]byte(rendered), &parsed)) + require.NotNil(t, parsed.Data.S3) + // Ambient credentials: no env_var references are emitted. + require.Nil(t, parsed.Data.S3.AccessKeyID) + require.Nil(t, parsed.Data.S3.SecretAccessKey) +} + +func TestRenderBufstreamConfigGCS(t *testing.T) { + storage := objectstore.ConnInfo{ + Provider: apiv2.ObjectStoreProviderGCS, + Path: "prefix", + Bucket: "wandb-bucket", + } + rendered, err := renderBufstreamConfig("k", "k.ns.svc", []string{"e:2379"}, storage) + require.NoError(t, err) + + var parsed bufstreamConfig + require.NoError(t, yaml.Unmarshal([]byte(rendered), &parsed)) + require.Nil(t, parsed.Data.S3) + require.Equal(t, "gs://wandb-bucket/prefix/k", parsed.Data.GCS) +} + +func TestRenderBufstreamConfigAzure(t *testing.T) { + storage := objectstore.ConnInfo{ + Provider: apiv2.ObjectStoreProviderAzure, + Bucket: "container", + AccessKey: "wandbstorageacct", + SecretKey: "azsupersecret", + } + rendered, err := renderBufstreamConfig("k", "k.ns.svc", []string{"e:2379"}, storage) + require.NoError(t, err) + + var parsed bufstreamConfig + require.NoError(t, yaml.Unmarshal([]byte(rendered), &parsed)) + require.Nil(t, parsed.Data.S3) + require.NotNil(t, parsed.Data.Azure) + require.Equal(t, "https://wandbstorageacct.blob.core.windows.net/container/k", parsed.Data.Azure.URI, + "the blob host must derive from the connection's storage account") + require.NotNil(t, parsed.Data.Azure.AccessKeyID) + require.Equal(t, EnvStorageAccessKeyID, parsed.Data.Azure.AccessKeyID.EnvVar) + require.NotContains(t, rendered, "azsupersecret") +} diff --git a/internal/controller/infra/managed/kafka/bufstream/conn.go b/internal/controller/infra/managed/kafka/bufstream/conn.go new file mode 100644 index 00000000..e0239dac --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/conn.go @@ -0,0 +1,99 @@ +package bufstream + +import ( + "context" + "fmt" + "strconv" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type kafkaConnInfo struct { + Host string + Port string +} + +func (c *kafkaConnInfo) toURL() string { + return fmt.Sprintf("kafka://%s:%s", c.Host, c.Port) +} + +func readConnectionDetails(nsnBuilder *NsNameBuilder) *kafkaConnInfo { + return &kafkaConnInfo{ + Host: nsnBuilder.BufstreamHost(), + Port: strconv.Itoa(KafkaListenerPort), + } +} + +func writeKafkaConnInfo( + ctx context.Context, + cl client.Client, + owner client.Object, + nsnBuilder *NsNameBuilder, + connInfo *kafkaConnInfo, +) (*apiv2.KafkaConnection, error) { + log := logx.GetSlog(ctx) + + var err error + var found bool + var gvk schema.GroupVersionKind + var actual = &corev1.Secret{} + + nsName := nsnBuilder.ConnectionNsName() + urlKey := "url" + + if found, err = common.GetResource(ctx, cl, nsName, AppConnTypeName, actual); err != nil { + return nil, err + } + if !found { + actual = nil + } + + var ownerRefs []metav1.OwnerReference + if owner.GetNamespace() == nsName.Namespace { + if gvk, err = cl.GroupVersionKindFor(owner); err != nil { + log.Error(fmt.Sprintf("Error getting GVK for %s", owner.GetName()), logx.ErrAttr(err)) + return nil, fmt.Errorf("could not get GVK for owner: %w", err) + } + ownerRefs = []metav1.OwnerReference{{ + APIVersion: gvk.GroupVersion().String(), + Kind: gvk.Kind, + Name: owner.GetName(), + UID: owner.GetUID(), + Controller: ptr.To(false), + BlockOwnerDeletion: ptr.To(false), + }} + } + + desired := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsName.Name, + Namespace: nsName.Namespace, + OwnerReferences: ownerRefs, + }, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{ + urlKey: connInfo.toURL(), + "Host": connInfo.Host, + "Port": connInfo.Port, + }, + } + + if _, err = common.CrudResource(ctx, cl, desired, actual); err != nil { + return nil, err + } + + localRef := corev1.LocalObjectReference{Name: nsName.Name} + return &apiv2.KafkaConnection{ + URL: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: urlKey, Optional: ptr.To(false)}, + Host: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Host", Optional: ptr.To(false)}, + Port: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Port", Optional: ptr.To(false)}, + BrokerEndpoint: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Host", Optional: ptr.To(false)}, + }, nil +} diff --git a/internal/controller/infra/managed/kafka/bufstream/detach.go b/internal/controller/infra/managed/kafka/bufstream/detach.go new file mode 100644 index 00000000..03e6cdd8 --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/detach.go @@ -0,0 +1,111 @@ +package bufstream + +import ( + "context" + "log/slog" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// CheckDetached blocks spec changes when the broker Application has been detached +// from the wandb CR but its replica count no longer matches the desired spec. +func CheckDetached( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + wandbUID types.UID, + desiredReplicas int32, +) []metav1.Condition { + nsnBuilder := createNsNameBuilder(specNamespacedName) + actual := &apiv2.Application{} + found, err := common.GetResource(ctx, cl, nsnBuilder.BufstreamNsName(), ApplicationResourceType, actual) + if err != nil || !found { + return nil + } + if !common.IsDetached(actual, wandbUID) { + return nil + } + actualReplicas := int32(0) + if actual.Spec.Replicas != nil { + actualReplicas = *actual.Spec.Replicas + } + // Compare against the effective (HA-floored) broker count, the same value + // ToBufstreamApplication builds the Deployment with. Otherwise a detached + // cluster sized below the floor (e.g. dev sizing requests 1, floor is 2) + // would always look mismatched and never get re-adopted on re-apply. + if actualReplicas != effectiveBufstreamReplicas(desiredReplicas) { + return []metav1.Condition{{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.DetachedSpecMismatch, + Message: "detached Bufstream Application spec mismatch", + }} + } + return nil +} + +func DetachFinalizer( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + wandbOwner client.Object, +) error { + ctx, log := logx.WithSlog(ctx, logx.Kafka) + nsnBuilder := createNsNameBuilder(specNamespacedName) + + apps := []types.NamespacedName{ + nsnBuilder.EtcdNsName(), + nsnBuilder.BufstreamNsName(), + } + for _, nn := range apps { + if err := detachObject(ctx, cl, log, nn, ApplicationResourceType, &apiv2.Application{}, wandbOwner); err != nil { + return err + } + } + + if err := detachObject(ctx, cl, log, nsnBuilder.ConfigMapNsName(), ConfigMapResourceType, &corev1.ConfigMap{}, wandbOwner); err != nil { + return err + } + if err := detachObject(ctx, cl, log, nsnBuilder.CredentialsNsName(), SecretResourceType, &corev1.Secret{}, wandbOwner); err != nil { + return err + } + return detachObject(ctx, cl, log, nsnBuilder.ConnectionNsName(), SecretResourceType, &corev1.Secret{}, wandbOwner) +} + +func detachObject( + ctx context.Context, + cl client.Client, + log *slog.Logger, + nsName types.NamespacedName, + resourceType string, + obj client.Object, + wandbOwner client.Object, +) error { + found, err := common.GetResource(ctx, cl, nsName, resourceType, obj) + if err != nil { + return err + } + if !found { + return nil + } + if common.IsDetached(obj, wandbOwner.GetUID()) { + return nil + } + common.RemoveOwnerReference(obj, wandbOwner.GetUID()) + if err := cl.Update(ctx, obj); err != nil { + if errors.IsNotFound(err) { + return nil + } + log.Error("error detaching resource", logx.ErrAttr(err), "name", nsName.Name) + return err + } + log.Info("detached resource", "type", resourceType, "name", nsName.Name) + return nil +} diff --git a/internal/controller/infra/managed/kafka/bufstream/detach_test.go b/internal/controller/infra/managed/kafka/bufstream/detach_test.go new file mode 100644 index 00000000..4d9a2cfb --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/detach_test.go @@ -0,0 +1,69 @@ +package bufstream + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func detachedBufstreamApp(replicas int32) *apiv2.Application { + return &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb-kafka", + Namespace: "default", + // no owner references => detached + }, + Spec: apiv2.ApplicationSpec{ + Replicas: ptr.To(replicas), + }, + } +} + +// A detached app sitting at the HA floor must be re-adoptable even though dev +// sizing requests fewer brokers than the floor, otherwise re-applying the CR +// can never reclaim the existing infra. +func TestCheckDetachedReadoptsBelowHAFloor(t *testing.T) { + cl := fake.NewClientBuilder(). + WithScheme(testScheme(t)). + WithObjects(detachedBufstreamApp(BufstreamReplicas)). + Build() + + nsn := types.NamespacedName{Namespace: "default", Name: "wandb-kafka"} + conds := CheckDetached(context.Background(), cl, nsn, types.UID("new-cr-uid"), 1) + require.Nil(t, conds) +} + +// A genuine size increase on a detached cluster must still block, so the +// operator never silently resizes infra a user deliberately detached. +func TestCheckDetachedBlocksRealMismatch(t *testing.T) { + cl := fake.NewClientBuilder(). + WithScheme(testScheme(t)). + WithObjects(detachedBufstreamApp(BufstreamReplicas)). + Build() + + nsn := types.NamespacedName{Namespace: "default", Name: "wandb-kafka"} + conds := CheckDetached(context.Background(), cl, nsn, types.UID("new-cr-uid"), 5) + require.Len(t, conds, 1) + require.Equal(t, common.DetachedSpecMismatch, conds[0].Reason) +} + +// An app still owned by the CR is not detached, so CheckDetached is a no-op. +func TestCheckDetachedSkipsWhenOwned(t *testing.T) { + app := detachedBufstreamApp(BufstreamReplicas) + app.OwnerReferences = []metav1.OwnerReference{{UID: types.UID("cr-uid")}} + cl := fake.NewClientBuilder(). + WithScheme(testScheme(t)). + WithObjects(app). + Build() + + nsn := types.NamespacedName{Namespace: "default", Name: "wandb-kafka"} + conds := CheckDetached(context.Background(), cl, nsn, types.UID("cr-uid"), 5) + require.Nil(t, conds) +} diff --git a/internal/controller/infra/managed/kafka/bufstream/image_test.go b/internal/controller/infra/managed/kafka/bufstream/image_test.go new file mode 100644 index 00000000..be51cda5 --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/image_test.go @@ -0,0 +1,53 @@ +package bufstream + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/wandb/operator/pkg/wandb/manifest" +) + +func TestBufstreamImage(t *testing.T) { + require.Equal(t, defaultBufstreamImage, BufstreamImage(manifest.ImageRef{}, "")) + require.Equal(t, + "us-docker.pkg.dev/buf-images-1/buf/images/bufstream:0.5.0", + BufstreamImage(manifest.ImageRef{ + Registry: "us-docker.pkg.dev", + Repository: "buf-images-1/buf/images/bufstream", + Tag: "0.5.0", + }, ""), + ) + require.Equal(t, + "myregistry.io/us-docker.pkg.dev/buf-images-1/buf/images/bufstream:0.5.0", + BufstreamImage(manifest.ImageRef{ + Registry: "us-docker.pkg.dev", + Repository: "buf-images-1/buf/images/bufstream", + Tag: "0.5.0", + }, "myregistry.io"), + ) +} + +func TestEtcdImage(t *testing.T) { + require.Equal(t, defaultEtcdImage, EtcdImage(manifest.ImageRef{}, "")) + require.Equal(t, + "quay.io/coreos/etcd:v3.5.31", + EtcdImage(manifest.ImageRef{Registry: "quay.io", Repository: "coreos/etcd", Tag: "v3.5.31"}, ""), + ) + require.Equal(t, + "myregistry.io/quay.io/coreos/etcd:v3.5.31", + EtcdImage(manifest.ImageRef{Registry: "quay.io", Repository: "coreos/etcd", Tag: "v3.5.31"}, "myregistry.io"), + ) +} + +func TestBucketEnsureImage(t *testing.T) { + require.Equal(t, defaultBucketEnsureImage, BucketEnsureImage(manifest.ImageRef{}, "")) + require.Equal(t, + "amazon/aws-cli:2.35.10", + BucketEnsureImage(manifest.ImageRef{Repository: "amazon/aws-cli", Tag: "2.35.10"}, ""), + ) + require.Equal(t, + "myregistry.io/amazon/aws-cli:2.35.10", + BucketEnsureImage(manifest.ImageRef{Repository: "amazon/aws-cli", Tag: "2.35.10"}, "myregistry.io"), + ) +} diff --git a/internal/controller/infra/managed/kafka/bufstream/naming.go b/internal/controller/infra/managed/kafka/bufstream/naming.go new file mode 100644 index 00000000..cc93fa60 --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/naming.go @@ -0,0 +1,151 @@ +package bufstream + +import ( + "fmt" + "strings" + + "github.com/wandb/operator/internal/controller/common" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation" +) + +// assumedMaxEtcdOrdinal covers quorum-sized etcd ensembles (1, 3, 5). +const assumedMaxEtcdOrdinal = 9 + +const defaultNameSuffix = "-kafka" + +// MaxSpecNameLength is the room left by the longest label-constrained derived +// name, the etcd pod name. +func MaxSpecNameLength() int { + builder := CreateNsNameBuilder(types.NamespacedName{}) + return validation.DNS1123LabelMaxLength - len(builder.EtcdPodName(assumedMaxEtcdOrdinal)) +} + +// DefaultSpecName derives the managed Kafka name for a CR instance, shortened +// to the budget. +func DefaultSpecName(crName, instanceKey string) string { + return common.FitDefaultInfraName(common.InstanceBaseName(crName, instanceKey), defaultNameSuffix, MaxSpecNameLength()) +} + +// NsNameBuilder derives the names of all resources that make up a managed +// Bufstream deployment from the base Kafka spec name/namespace. +type NsNameBuilder struct { + baseNsName types.NamespacedName +} + +func CreateNsNameBuilder(baseNsName types.NamespacedName) *NsNameBuilder { + return &NsNameBuilder{baseNsName: baseNsName} +} + +func (n *NsNameBuilder) Namespace() string { + return n.baseNsName.Namespace +} + +func (n *NsNameBuilder) SpecName() string { + return n.baseNsName.Name +} + +// BufstreamName is the broker Deployment/Service name and the Kafka bootstrap host. +func (n *NsNameBuilder) BufstreamName() string { + return n.SpecName() +} + +func (n *NsNameBuilder) BufstreamNsName() types.NamespacedName { + return types.NamespacedName{Namespace: n.Namespace(), Name: n.BufstreamName()} +} + +// BufstreamHost is the in-cluster DNS name clients use to reach the brokers. +func (n *NsNameBuilder) BufstreamHost() string { + return fmt.Sprintf("%s.%s.svc.cluster.local", n.BufstreamName(), n.Namespace()) +} + +// ServiceAccountName is the shared etcd/Bufstream identity for the SCC grant. +func (n *NsNameBuilder) ServiceAccountName() string { + return n.SpecName() +} + +func (n *NsNameBuilder) ServiceAccountNsName() types.NamespacedName { + return types.NamespacedName{Namespace: n.Namespace(), Name: n.ServiceAccountName()} +} + +// SccRoleBindingName grants the Kafka SA use of nonroot-v2 (OpenShift only). +func (n *NsNameBuilder) SccRoleBindingName() string { + return fmt.Sprintf("%s-scc-nonroot-v2", n.SpecName()) +} + +func (n *NsNameBuilder) SccRoleBindingNsName() types.NamespacedName { + return types.NamespacedName{Namespace: n.Namespace(), Name: n.SccRoleBindingName()} +} + +func (n *NsNameBuilder) ConfigMapName() string { + return fmt.Sprintf("%s-config", n.SpecName()) +} + +func (n *NsNameBuilder) ConfigMapNsName() types.NamespacedName { + return types.NamespacedName{Namespace: n.Namespace(), Name: n.ConfigMapName()} +} + +func (n *NsNameBuilder) CredentialsName() string { + return fmt.Sprintf("%s-storage-credentials", n.SpecName()) +} + +func (n *NsNameBuilder) CredentialsNsName() types.NamespacedName { + return types.NamespacedName{Namespace: n.Namespace(), Name: n.CredentialsName()} +} + +func (n *NsNameBuilder) EtcdName() string { + return fmt.Sprintf("%s-etcd", n.SpecName()) +} + +func (n *NsNameBuilder) EtcdNsName() types.NamespacedName { + return types.NamespacedName{Namespace: n.Namespace(), Name: n.EtcdName()} +} + +// EtcdHost is the headless Service domain that governs the etcd StatefulSet. +// Individual members are addressed as <pod>.<EtcdHost>. +func (n *NsNameBuilder) EtcdHost() string { + return fmt.Sprintf("%s.%s.svc.cluster.local", n.EtcdName(), n.Namespace()) +} + +// EtcdPodName is the stable StatefulSet pod name for member ordinal i. It +// doubles as the etcd member name (ETCD_NAME). +func (n *NsNameBuilder) EtcdPodName(i int) string { + return fmt.Sprintf("%s-%d", n.EtcdName(), i) +} + +// EtcdPodFQDN is the per-pod stable DNS name for member ordinal i. +func (n *NsNameBuilder) EtcdPodFQDN(i int) string { + return fmt.Sprintf("%s.%s", n.EtcdPodName(i), n.EtcdHost()) +} + +// EtcdInitialCluster renders the static ETCD_INITIAL_CLUSTER membership list +// (member=peerURL pairs) for a cluster of the given size. +func (n *NsNameBuilder) EtcdInitialCluster(replicas, peerPort int) string { + members := make([]string, 0, replicas) + for i := 0; i < replicas; i++ { + members = append(members, fmt.Sprintf("%s=http://%s:%d", n.EtcdPodName(i), n.EtcdPodFQDN(i), peerPort)) + } + return strings.Join(members, ",") +} + +// EtcdClientEndpoints lists the per-member client URLs that the broker connects +// to, giving it every endpoint for failover rather than a single VIP. +func (n *NsNameBuilder) EtcdClientEndpoints(replicas, clientPort int) []string { + endpoints := make([]string, 0, replicas) + for i := 0; i < replicas; i++ { + endpoints = append(endpoints, fmt.Sprintf("%s:%d", n.EtcdPodFQDN(i), clientPort)) + } + return endpoints +} + +func (n *NsNameBuilder) ConnectionName() string { + return fmt.Sprintf("%s-connection", n.SpecName()) +} + +func (n *NsNameBuilder) ConnectionNsName() types.NamespacedName { + return types.NamespacedName{Namespace: n.Namespace(), Name: n.ConnectionName()} +} + +func createNsNameBuilder(baseNsName types.NamespacedName) *NsNameBuilder { + return CreateNsNameBuilder(baseNsName) +} diff --git a/internal/controller/infra/managed/kafka/bufstream/purge.go b/internal/controller/infra/managed/kafka/bufstream/purge.go new file mode 100644 index 00000000..50e6a3d2 --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/purge.go @@ -0,0 +1,58 @@ +package bufstream + +import ( + "context" + + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// PurgeFinalizer deletes the etcd PVCs when the retention policy is Purge. The +// Application CRs, ConfigMap and Secret are owner-referenced by the wandb CR and +// garbage collected automatically, but StatefulSet PVCs are not. +func PurgeFinalizer( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + onDeleteRule common.OnDeleteRule, +) error { + ctx, _ = logx.WithSlog(ctx, logx.Kafka) + if onDeleteRule.Policy != common.Purge { + return nil + } + return purgeAssociatedResources(ctx, cl, specNamespacedName.Namespace, onDeleteRule.Selector) +} + +func purgeAssociatedResources( + ctx context.Context, + cl client.Client, + namespace string, + onDeleteSelector labels.Selector, +) error { + log := logx.GetSlog(ctx) + listOptions := &client.ListOptions{ + Namespace: namespace, + LabelSelector: onDeleteSelector, + } + + pvcList := &corev1.PersistentVolumeClaimList{} + if err := cl.List(ctx, pvcList, listOptions); err != nil { + return err + } + if len(pvcList.Items) > 0 { + log.Info("Purging associated PVCs", "count", len(pvcList.Items), "selector", onDeleteSelector.String()) + } + for i := range pvcList.Items { + pvc := &pvcList.Items[i] + if err := cl.Delete(ctx, pvc); err != nil && !errors.IsNotFound(err) { + return err + } + } + + return nil +} diff --git a/internal/controller/infra/managed/kafka/bufstream/read.go b/internal/controller/infra/managed/kafka/bufstream/read.go new file mode 100644 index 00000000..e508e09c --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/read.go @@ -0,0 +1,89 @@ +package bufstream + +import ( + "context" + + apiv2 "github.com/wandb/operator/api/v2" + ctrlcommon "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func ReadState( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + wandbOwner client.Object, + onDeleteRule ctrlcommon.OnDeleteRule, +) ([]metav1.Condition, *apiv2.KafkaConnection) { + ctx, log := logx.WithSlog(ctx, logx.Kafka) + nsnBuilder := createNsNameBuilder(specNamespacedName) + + bufApp := &apiv2.Application{} + found, err := ctrlcommon.GetResource(ctx, cl, nsnBuilder.BufstreamNsName(), ApplicationResourceType, bufApp) + if err != nil { + return []metav1.Condition{{ + Type: BufstreamApplicationType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }}, nil + } + + conditions := make([]metav1.Condition, 0) + + if !found { + log.Info("Bufstream Application CR not found") + bufApp = nil + if onDeleteRule.Policy == ctrlcommon.Purge { + if err := purgeAssociatedResources(ctx, cl, specNamespacedName.Namespace, onDeleteRule.Selector); err != nil { + conditions = append(conditions, metav1.Condition{ + Type: BufstreamApplicationType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }) + } else { + conditions = append(conditions, metav1.Condition{ + Type: BufstreamApplicationType, + Status: metav1.ConditionFalse, + Reason: ctrlcommon.PendingDeleteReason, + }) + } + } + return conditions, nil + } + + etcdApp := &apiv2.Application{} + etcdReady := false + if etcdFound, etcdErr := ctrlcommon.GetResource(ctx, cl, nsnBuilder.EtcdNsName(), ApplicationResourceType, etcdApp); etcdErr == nil && etcdFound { + etcdReady = etcdApp.Status.Ready + } + + connInfo := readConnectionDetails(nsnBuilder) + connection, err := writeKafkaConnInfo(ctx, cl, wandbOwner, nsnBuilder, connInfo) + if err != nil { + return []metav1.Condition{{ + Type: KafkaConnectionInfoType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }}, nil + } + conditions = append(conditions, metav1.Condition{ + Type: KafkaConnectionInfoType, + Status: metav1.ConditionTrue, + Reason: ctrlcommon.ResourceExistsReason, + }) + + reportedReady := metav1.ConditionFalse + if bufApp.Status.Ready && etcdReady { + reportedReady = metav1.ConditionTrue + } + conditions = append(conditions, metav1.Condition{ + Type: KafkaReportedReadyType, + Status: reportedReady, + Reason: ctrlcommon.ReportedStatusReason, + }) + + return conditions, connection +} diff --git a/internal/controller/infra/managed/kafka/bufstream/spec.go b/internal/controller/infra/managed/kafka/bufstream/spec.go new file mode 100644 index 00000000..aee773de --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/spec.go @@ -0,0 +1,604 @@ +package bufstream + +import ( + "fmt" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/external/objectstore" + "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" +) + +const defaultEtcdStorageSize = "10Gi" + +const ( + kafkaRunAsUser int64 = 65532 + kafkaRunAsGroup int64 = 65532 + kafkaFSGroup int64 = 65532 + + kafkaCapabilityAll corev1.Capability = "ALL" +) + +const ( + // Keys for the manifest's kafka.images map. Every image the managed Bufstream + // deployment pulls is sourced from this map, keyed by component. + imageKeyBufstream = "bufstream" + imageKeyEtcd = "etcd" + imageKeyBucketEnsure = "bucketEnsure" + + bucketEnsureMaxAttempts = 150 + bucketEnsureDelaySeconds = 2 +) + +// BufstreamImage resolves the Bufstream broker image from the manifest, falling +// back to the hardcoded default when the manifest does not supply one. +func BufstreamImage(img manifest.ImageRef, globalImageRegistry string) string { + return resolveImage(img, globalImageRegistry, defaultBufstreamImage) +} + +// EtcdImage resolves the etcd metadata-store image from the manifest, falling +// back to the hardcoded default when the manifest does not supply one. +func EtcdImage(img manifest.ImageRef, globalImageRegistry string) string { + return resolveImage(img, globalImageRegistry, defaultEtcdImage) +} + +// BucketEnsureImage resolves the bucket-ensure init-container image from the +// manifest, falling back to the hardcoded default when the manifest does not +// supply one. +func BucketEnsureImage(img manifest.ImageRef, globalImageRegistry string) string { + return resolveImage(img, globalImageRegistry, defaultBucketEnsureImage) +} + +func resolveImage(img manifest.ImageRef, globalImageRegistry, fallback string) string { + if out := img.GetImage(globalImageRegistry); out != "" { + return out + } + // Fallback for older manifests that don't supply the image. + return fallback +} + +func BuildWandbKafkaLabels(wandb *apiv2.WeightsAndBiases) map[string]string { + return common.BuildWandbLabels(wandb, KafkaModuleName) +} + +func ToKafkaOnDeleteRule(wandb *apiv2.WeightsAndBiases, retentionPolicy apiv2.RetentionPolicy) common.OnDeleteRule { + return common.ToOnDeleteRule(wandb, retentionPolicy, KafkaModuleName) +} + +// kafkaPodSecurityContext: etcd omits fixed IDs on OpenShift, pins them else. +func kafkaPodSecurityContext() *corev1.PodSecurityContext { + if utils.IsOpenShift() { + return &corev1.PodSecurityContext{ + RunAsNonRoot: ptr.To(true), + SeccompProfile: kafkaRuntimeDefaultSeccompProfile(), + } + } + return bufstreamPodSecurityContext() +} + +func kafkaContainerSecurityContext() *corev1.SecurityContext { + if utils.IsOpenShift() { + return &corev1.SecurityContext{ + RunAsNonRoot: ptr.To(true), + AllowPrivilegeEscalation: ptr.To(false), + Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{kafkaCapabilityAll}}, + SeccompProfile: kafkaRuntimeDefaultSeccompProfile(), + } + } + return bufstreamContainerSecurityContext() +} + +// bufstreamPodSecurityContext pins UID/GID 65532 (its 0700 binary needs it). +func bufstreamPodSecurityContext() *corev1.PodSecurityContext { + return &corev1.PodSecurityContext{ + RunAsUser: ptr.To(kafkaRunAsUser), + RunAsGroup: ptr.To(kafkaRunAsGroup), + RunAsNonRoot: ptr.To(true), + FSGroup: ptr.To(kafkaFSGroup), + FSGroupChangePolicy: ptr.To(corev1.FSGroupChangeOnRootMismatch), + SeccompProfile: kafkaRuntimeDefaultSeccompProfile(), + } +} + +func bufstreamContainerSecurityContext() *corev1.SecurityContext { + return &corev1.SecurityContext{ + RunAsUser: ptr.To(kafkaRunAsUser), + RunAsGroup: ptr.To(kafkaRunAsGroup), + RunAsNonRoot: ptr.To(true), + AllowPrivilegeEscalation: ptr.To(false), + Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{kafkaCapabilityAll}}, + SeccompProfile: kafkaRuntimeDefaultSeccompProfile(), + } +} + +func kafkaRuntimeDefaultSeccompProfile() *corev1.SeccompProfile { + return &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault} +} + +// sameNamespace reports whether the managed Kafka resources live in the same +// namespace as the owning WeightsAndBiases CR. Owner references are only valid +// within a single namespace. +func sameNamespace(wandb *apiv2.WeightsAndBiases, nsnBuilder *NsNameBuilder) bool { + return wandb.Namespace == nsnBuilder.Namespace() +} + +func setOwner(wandb *apiv2.WeightsAndBiases, obj metav1.Object, nsnBuilder *NsNameBuilder, scheme *runtime.Scheme) error { + if !sameNamespace(wandb, nsnBuilder) { + return nil + } + return ctrl.SetControllerReference(wandb, obj, scheme) +} + +func intstrFromInt(port int) intstr.IntOrString { + return intstr.FromInt32(int32(port)) +} + +// tolerations safely dereferences the wandb tolerations pointer, which may be +// nil when neither the component nor the CR specify any. +func tolerations(wandb *apiv2.WeightsAndBiases, spec apiv2.ManagedInfraSpec) []corev1.Toleration { + if t := wandb.GetTolerations(spec); t != nil { + return *t + } + return nil +} + +// ToCredentialsSecret builds the secret that holds the object-store credentials +// referenced by the broker config's env_var data sources. +func ToCredentialsSecret( + wandb *apiv2.WeightsAndBiases, + nsnBuilder *NsNameBuilder, + storage objectstore.ConnInfo, + scheme *runtime.Scheme, +) (*corev1.Secret, error) { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsnBuilder.CredentialsName(), + Namespace: nsnBuilder.Namespace(), + Labels: BuildWandbKafkaLabels(wandb), + }, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{ + EnvStorageAccessKeyID: storage.AccessKey, + EnvStorageSecretAccessKey: storage.SecretKey, + }, + } + if err := setOwner(wandb, secret, nsnBuilder, scheme); err != nil { + return nil, err + } + return secret, nil +} + +// ToConfigMap renders the bufstream.yaml into a ConfigMap. +func ToConfigMap( + wandb *apiv2.WeightsAndBiases, + nsnBuilder *NsNameBuilder, + storage objectstore.ConnInfo, + scheme *runtime.Scheme, +) (*corev1.ConfigMap, error) { + rendered, err := renderBufstreamConfig( + nsnBuilder.BufstreamName(), + nsnBuilder.BufstreamHost(), + nsnBuilder.EtcdClientEndpoints(EtcdReplicas, EtcdClientPort), + storage, + ) + if err != nil { + return nil, err + } + + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsnBuilder.ConfigMapName(), + Namespace: nsnBuilder.Namespace(), + Labels: BuildWandbKafkaLabels(wandb), + }, + Data: map[string]string{ConfigFileName: rendered}, + } + if err := setOwner(wandb, cm, nsnBuilder, scheme); err != nil { + return nil, err + } + return cm, nil +} + +// ToServiceAccount builds the dedicated etcd/Bufstream SA for the SCC grant. +func ToServiceAccount( + wandb *apiv2.WeightsAndBiases, + nsnBuilder *NsNameBuilder, + scheme *runtime.Scheme, +) (*corev1.ServiceAccount, error) { + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsnBuilder.ServiceAccountName(), + Namespace: nsnBuilder.Namespace(), + Labels: BuildWandbKafkaLabels(wandb), + }, + AutomountServiceAccountToken: ptr.To(false), + } + if err := setOwner(wandb, sa, nsnBuilder, scheme); err != nil { + return nil, err + } + return sa, nil +} + +// ToSccRoleBinding binds the Kafka SA to nonroot-v2 for UID 65532 (OpenShift). +func ToSccRoleBinding( + wandb *apiv2.WeightsAndBiases, + nsnBuilder *NsNameBuilder, + scheme *runtime.Scheme, +) (*rbacv1.RoleBinding, error) { + rb := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsnBuilder.SccRoleBindingName(), + Namespace: nsnBuilder.Namespace(), + Labels: BuildWandbKafkaLabels(wandb), + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "ClusterRole", + Name: nonRootV2SCCClusterRole, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: nsnBuilder.ServiceAccountName(), + Namespace: nsnBuilder.Namespace(), + }, + }, + } + if err := setOwner(wandb, rb, nsnBuilder, scheme); err != nil { + return nil, err + } + return rb, nil +} + +// ToEtcdApplication builds the Application CR that deploys etcd as a highly +// available StatefulSet: an odd-sized cluster (EtcdReplicas) fronted by a +// headless Service that gives each member a stable peer DNS identity. +func ToEtcdApplication( + wandb *apiv2.WeightsAndBiases, + nsnBuilder *NsNameBuilder, + scheme *runtime.Scheme, + mfst manifest.Manifest, +) (*apiv2.Application, error) { + infraSpec := wandb.Spec.Kafka.ManagedKafka + labels := BuildWandbKafkaLabels(wandb) + + storageSize := infraSpec.StorageSize + if storageSize == "" { + storageSize = defaultEtcdStorageSize + } + quantity, err := resource.ParseQuantity(storageSize) + if err != nil { + return nil, fmt.Errorf("invalid etcd storage size %q: %w", storageSize, err) + } + + // Each pod derives its own member identity from its stable StatefulSet name + // via the downward API. Kubernetes expands $(POD_NAME) in later env values, + // so peer/client advertise URLs resolve to the per-pod headless DNS record. + // The static ETCD_INITIAL_CLUSTER lists every member up front so the cluster + // bootstraps without external discovery. + etcdEnv := []corev1.EnvVar{ + {Name: "POD_NAME", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.name"}}}, + {Name: "ETCD_NAME", Value: "$(POD_NAME)"}, + {Name: "ETCD_DATA_DIR", Value: EtcdDataDir}, + {Name: "ETCD_LISTEN_CLIENT_URLS", Value: fmt.Sprintf("http://0.0.0.0:%d", EtcdClientPort)}, + {Name: "ETCD_ADVERTISE_CLIENT_URLS", Value: fmt.Sprintf("http://$(POD_NAME).%s:%d", nsnBuilder.EtcdHost(), EtcdClientPort)}, + {Name: "ETCD_LISTEN_PEER_URLS", Value: fmt.Sprintf("http://0.0.0.0:%d", EtcdPeerPort)}, + {Name: "ETCD_INITIAL_ADVERTISE_PEER_URLS", Value: fmt.Sprintf("http://$(POD_NAME).%s:%d", nsnBuilder.EtcdHost(), EtcdPeerPort)}, + {Name: "ETCD_INITIAL_CLUSTER", Value: nsnBuilder.EtcdInitialCluster(EtcdReplicas, EtcdPeerPort)}, + {Name: "ETCD_INITIAL_CLUSTER_STATE", Value: "new"}, + {Name: "ETCD_INITIAL_CLUSTER_TOKEN", Value: fmt.Sprintf("%s-%s", EtcdClusterToken, nsnBuilder.SpecName())}, + {Name: "ETCD_AUTO_COMPACTION_MODE", Value: "periodic"}, + {Name: "ETCD_AUTO_COMPACTION_RETENTION", Value: "30s"}, + } + + app := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsnBuilder.EtcdName(), + Namespace: nsnBuilder.Namespace(), + Labels: labels, + }, + Spec: apiv2.ApplicationSpec{ + Kind: "StatefulSet", + Replicas: ptr.To(int32(EtcdReplicas)), + ServiceName: nsnBuilder.EtcdName(), + MetaTemplate: metav1.ObjectMeta{ + Labels: labels, + }, + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + ServiceAccountName: nsnBuilder.ServiceAccountName(), + AutomountServiceAccountToken: ptr.To(false), + SecurityContext: kafkaPodSecurityContext(), + Affinity: spreadAffinity(wandb, infraSpec.ManagedInfraSpec, labels), + Tolerations: tolerations(wandb, infraSpec.ManagedInfraSpec), + Containers: []corev1.Container{ + { + Name: "etcd", + Image: EtcdImage(mfst.Kafka.Images[imageKeyEtcd], wandb.Spec.Global.ImageRegistry), + Env: etcdEnv, + SecurityContext: kafkaContainerSecurityContext(), + Ports: []corev1.ContainerPort{ + {Name: "client", ContainerPort: EtcdClientPort}, + {Name: "peer", ContainerPort: EtcdPeerPort}, + }, + VolumeMounts: []corev1.VolumeMount{ + {Name: EtcdDataVolumeName, MountPath: EtcdDataDir}, + }, + ReadinessProbe: etcdProbe(), + LivenessProbe: etcdProbe(), + }, + }, + }, + }, + VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{Name: EtcdDataVolumeName}, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{corev1.ResourceStorage: quantity}, + }, + }, + }, + }, + // Headless Service (ClusterIP None) backs the StatefulSet's per-pod + // DNS. PublishNotReadyAddresses lets members resolve each other + // during bootstrap, before quorum makes them Ready. + ServiceTemplate: &corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: corev1.ClusterIPNone, + PublishNotReadyAddresses: true, + Selector: map[string]string{}, + Ports: []corev1.ServicePort{ + {Name: "client", Port: EtcdClientPort, TargetPort: intstrFromInt(EtcdClientPort)}, + {Name: "peer", Port: EtcdPeerPort, TargetPort: intstrFromInt(EtcdPeerPort)}, + }, + }, + }, + } + + if err := setOwner(wandb, app, nsnBuilder, scheme); err != nil { + return nil, err + } + return app, nil +} + +// etcdProbe checks the client port. A member only accepts client connections +// once it has joined the cluster, so this doubles as a readiness and liveness +// signal without requiring etcdctl in the image. +func etcdProbe() *corev1.Probe { + return &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{Port: intstrFromInt(EtcdClientPort)}, + }, + InitialDelaySeconds: 10, + PeriodSeconds: 10, + TimeoutSeconds: 5, + FailureThreshold: 6, + } +} + +// spreadAffinity preserves any operator/CR-provided affinity, otherwise it +// spreads the component's pods across nodes so a single node failure cannot take +// down quorum (etcd) or all brokers (Bufstream). GetAffinity may return a +// non-nil but empty Affinity (from the CR's global affinity field), so an empty +// value is treated the same as unset and gets the default spread. +func spreadAffinity(wandb *apiv2.WeightsAndBiases, spec apiv2.ManagedInfraSpec, labels map[string]string) *corev1.Affinity { + if a := wandb.GetAffinity(spec); a != nil && (a.NodeAffinity != nil || a.PodAffinity != nil || a.PodAntiAffinity != nil) { + return a + } + return &corev1.Affinity{ + PodAntiAffinity: &corev1.PodAntiAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{ + { + Weight: 100, + PodAffinityTerm: corev1.PodAffinityTerm{ + TopologyKey: "kubernetes.io/hostname", + LabelSelector: &metav1.LabelSelector{MatchLabels: labels}, + }, + }, + }, + }, + } +} + +// bucketEnsureContainer returns an init container that idempotently creates the +// object-store bucket Bufstream reads from on startup. Bufstream itself never +// creates the bucket, and it can come up before the W&B applications that would +// otherwise create it, so it must be ensured here. +func bucketEnsureContainer(nsnBuilder *NsNameBuilder, storage objectstore.ConnInfo, img manifest.ImageRef, globalImageRegistry string) corev1.Container { + region := storage.Region + if region == "" { + region = "us-east-1" + } + credsName := nsnBuilder.CredentialsName() + // Retry in this process so transient DNS and API startup failures do not + // become init-container restarts subject to kubelet exponential backoff. + script := fmt.Sprintf(`attempt=1 +while [ "$attempt" -le %d ]; do + if { aws --endpoint-url "$1" s3api head-bucket --bucket "$2" || aws --endpoint-url "$1" s3api create-bucket --bucket "$2"; } >/dev/null 2>&1; then + exit 0 + fi + attempt=$((attempt + 1)) + if [ "$attempt" -le %d ]; then sleep %d; fi +done +echo 'object-store bucket did not become ready before timeout' >&2 +exit 1`, bucketEnsureMaxAttempts, bucketEnsureMaxAttempts, bucketEnsureDelaySeconds) + return corev1.Container{ + Name: "ensure-bucket", + Image: BucketEnsureImage(img, globalImageRegistry), + Command: []string{"/bin/sh", "-c"}, + Args: []string{script, "ensure-bucket", storage.Endpoint, storage.Bucket}, + SecurityContext: kafkaContainerSecurityContext(), + Env: []corev1.EnvVar{ + {Name: "AWS_REGION", Value: region}, + { + Name: "AWS_ACCESS_KEY_ID", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: credsName}, + Key: EnvStorageAccessKeyID, + }, + }, + }, + { + Name: "AWS_SECRET_ACCESS_KEY", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: credsName}, + Key: EnvStorageSecretAccessKey, + }, + }, + }, + }, + } +} + +// storageCredentialEnv injects the object-store credentials into the broker when +// static keys exist. Providers that authenticate via ambient identity (AWS IAM +// roles, GCS workload identity) get no env vars and no secret reference. +func storageCredentialEnv(nsnBuilder *NsNameBuilder, storage objectstore.ConnInfo) []corev1.EnvVar { + if !storage.HasStaticCredentials() { + return nil + } + credsName := nsnBuilder.CredentialsName() + return []corev1.EnvVar{ + { + Name: EnvStorageAccessKeyID, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: credsName}, + Key: EnvStorageAccessKeyID, + }, + }, + }, + { + Name: EnvStorageSecretAccessKey, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: credsName}, + Key: EnvStorageSecretAccessKey, + }, + }, + }, + } +} + +// needsBucketEnsure reports whether a connection supports the managed +// S3 bucket initializer. The caller separately verifies that the selected +// object store is operator-managed so BYOB endpoints are never mutated. +func needsBucketEnsure(storage objectstore.ConnInfo) bool { + return storage.Provider == apiv2.ObjectStoreProviderS3 && storage.Endpoint != "" +} + +// effectiveBufstreamReplicas applies the HA floor to a requested broker count. +// BufstreamReplicas is an HA floor, not just a zero-value default: manifest infra +// sizing (ApplyInfraSizing) may set Replicas to 1 for small/dev sizes, but +// Bufstream brokers are stateless and must stay HA, so never run fewer than the +// floor. Larger sizes that request more brokers are still honored. This is the +// single source of truth for the desired broker count, so detach re-adoption +// (CheckDetached) compares against the same value the Deployment is built with. +func effectiveBufstreamReplicas(requested int32) int32 { + if requested < BufstreamReplicas { + return BufstreamReplicas + } + return requested +} + +// ToBufstreamApplication builds the Application CR that deploys the stateless +// Bufstream brokers as a Deployment. +func ToBufstreamApplication( + wandb *apiv2.WeightsAndBiases, + nsnBuilder *NsNameBuilder, + storage objectstore.ConnInfo, + ensureBucket bool, + scheme *runtime.Scheme, + mfst manifest.Manifest, +) (*apiv2.Application, error) { + infraSpec := wandb.Spec.Kafka.ManagedKafka + labels := BuildWandbKafkaLabels(wandb) + + replicas := effectiveBufstreamReplicas(infraSpec.Replicas) + + container := corev1.Container{ + Name: "bufstream", + Image: BufstreamImage(mfst.Kafka.Images[imageKeyBufstream], wandb.Spec.Global.ImageRegistry), + Args: []string{"serve", "--config", fmt.Sprintf("%s/%s", ConfigMountPath, ConfigFileName)}, + SecurityContext: bufstreamContainerSecurityContext(), + Ports: []corev1.ContainerPort{ + {Name: "kafka", ContainerPort: KafkaListenerPort}, + {Name: "metrics", ContainerPort: DebugPort}, + {Name: "admin", ContainerPort: AdminPort}, + }, + Env: storageCredentialEnv(nsnBuilder, storage), + VolumeMounts: []corev1.VolumeMount{ + {Name: "config", MountPath: ConfigMountPath, ReadOnly: true}, + }, + } + if len(infraSpec.Config.Resources.Requests) > 0 || len(infraSpec.Config.Resources.Limits) > 0 { + container.Resources = infraSpec.Config.Resources + } + + var initContainers []corev1.Container + if ensureBucket && needsBucketEnsure(storage) { + initContainers = append(initContainers, bucketEnsureContainer(nsnBuilder, storage, mfst.Kafka.Images[imageKeyBucketEnsure], wandb.Spec.Global.ImageRegistry)) + } + + app := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsnBuilder.BufstreamName(), + Namespace: nsnBuilder.Namespace(), + Labels: labels, + }, + Spec: apiv2.ApplicationSpec{ + Kind: "Deployment", + Replicas: ptr.To(replicas), + MetaTemplate: metav1.ObjectMeta{ + Labels: labels, + }, + PodTemplate: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + ServiceAccountName: nsnBuilder.ServiceAccountName(), + AutomountServiceAccountToken: ptr.To(false), + SecurityContext: bufstreamPodSecurityContext(), + Affinity: spreadAffinity(wandb, infraSpec.ManagedInfraSpec, labels), + Tolerations: tolerations(wandb, infraSpec.ManagedInfraSpec), + InitContainers: initContainers, + Containers: []corev1.Container{container}, + Volumes: []corev1.Volume{ + { + Name: "config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: nsnBuilder.ConfigMapName()}, + }, + }, + }, + }, + }, + }, + ServiceTemplate: &corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Selector: map[string]string{}, + Ports: []corev1.ServicePort{ + {Name: "kafka", Port: KafkaListenerPort, TargetPort: intstrFromInt(KafkaListenerPort)}, + {Name: "metrics", Port: DebugPort, TargetPort: intstrFromInt(DebugPort)}, + }, + }, + }, + } + + if err := setOwner(wandb, app, nsnBuilder, scheme); err != nil { + return nil, err + } + return app, nil +} diff --git a/internal/controller/infra/managed/kafka/bufstream/spec_test.go b/internal/controller/infra/managed/kafka/bufstream/spec_test.go new file mode 100644 index 00000000..344d4ea4 --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/spec_test.go @@ -0,0 +1,330 @@ +package bufstream + +import ( + "testing" + + "github.com/stretchr/testify/require" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/infra/external/objectstore" + "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +func testScheme(t *testing.T) *runtime.Scheme { + t.Helper() + scheme := runtime.NewScheme() + require.NoError(t, apiv2.AddToScheme(scheme)) + return scheme +} + +func testWandb() *apiv2.WeightsAndBiases { + return &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default"}, + Spec: apiv2.WeightsAndBiasesSpec{ + Kafka: apiv2.KafkaSpec{ + ManagedKafka: &apiv2.ManagedKafkaSpec{ + Name: "wandb-kafka", + Namespace: "default", + Replicas: 2, + StorageSize: "20Gi", + }, + }, + }, + } +} + +func setOpenShiftMode(t *testing.T, enabled bool) { + t.Helper() + utils.SetOpenShiftMode(enabled) + t.Cleanup(func() { + utils.SetOpenShiftMode(false) + }) +} + +func TestToEtcdApplication(t *testing.T) { + setOpenShiftMode(t, false) + wandb := testWandb() + nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) + + app, err := ToEtcdApplication(wandb, nsn, testScheme(t), manifest.Manifest{}) + require.NoError(t, err) + + require.Equal(t, "wandb-kafka-etcd", app.Name) + require.Equal(t, "StatefulSet", app.Spec.Kind) + require.Len(t, app.Spec.VolumeClaimTemplates, 1) + require.Equal(t, EtcdDataVolumeName, app.Spec.VolumeClaimTemplates[0].Name) + require.Equal(t, "20Gi", app.Spec.VolumeClaimTemplates[0].Spec.Resources.Requests.Storage().String()) + require.Len(t, app.Spec.PodTemplate.Spec.Containers, 1) + require.Equal(t, defaultEtcdImage, app.Spec.PodTemplate.Spec.Containers[0].Image) + requireKafkaPodSecurityContext(t, app.Spec.PodTemplate.Spec.SecurityContext) + requireKafkaContainerSecurityContext(t, app.Spec.PodTemplate.Spec.Containers[0].SecurityContext) + require.NotNil(t, app.Spec.ServiceTemplate) +} + +func TestToEtcdApplicationHA(t *testing.T) { + setOpenShiftMode(t, false) + wandb := testWandb() + nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) + + app, err := ToEtcdApplication(wandb, nsn, testScheme(t), manifest.Manifest{}) + require.NoError(t, err) + + // Odd-sized HA cluster fronted by a headless service. + require.NotNil(t, app.Spec.Replicas) + require.Equal(t, int32(EtcdReplicas), *app.Spec.Replicas) + require.Equal(t, "wandb-kafka-etcd", app.Spec.ServiceName) + require.Equal(t, corev1.ClusterIPNone, app.Spec.ServiceTemplate.ClusterIP) + require.True(t, app.Spec.ServiceTemplate.PublishNotReadyAddresses) + + // Each member derives its identity from the downward API and the static + // membership list contains every peer. + env := map[string]corev1.EnvVar{} + for _, e := range app.Spec.PodTemplate.Spec.Containers[0].Env { + env[e.Name] = e + } + require.NotNil(t, env["POD_NAME"].ValueFrom) + require.Equal(t, "metadata.name", env["POD_NAME"].ValueFrom.FieldRef.FieldPath) + require.Equal(t, "$(POD_NAME)", env["ETCD_NAME"].Value) + + initialCluster := env["ETCD_INITIAL_CLUSTER"].Value + for i := 0; i < EtcdReplicas; i++ { + member := nsn.EtcdPodName(i) + require.Contains(t, initialCluster, member+"=http://"+nsn.EtcdPodFQDN(i)) + } + + // Anti-affinity spreads members across nodes by default. + require.NotNil(t, app.Spec.PodTemplate.Spec.Affinity) + require.NotNil(t, app.Spec.PodTemplate.Spec.Affinity.PodAntiAffinity) + + require.NotNil(t, app.Spec.PodTemplate.Spec.Containers[0].ReadinessProbe) +} + +func testStorage() objectstore.ConnInfo { + return objectstore.ConnInfo{ + Provider: apiv2.ObjectStoreProviderS3, + URI: "s3://bucket", + Bucket: "bucket", + Endpoint: "http://seaweedfs:80", + Region: "us-east-1", + AccessKey: "ak", + SecretKey: "sk", + ForcePathStyle: true, + } +} + +func TestToBufstreamApplication(t *testing.T) { + setOpenShiftMode(t, false) + wandb := testWandb() + nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) + + app, err := ToBufstreamApplication(wandb, nsn, testStorage(), true, testScheme(t), manifest.Manifest{}) + require.NoError(t, err) + + require.Equal(t, "wandb-kafka", app.Name) + require.Equal(t, "Deployment", app.Spec.Kind) + require.NotNil(t, app.Spec.Replicas) + require.Len(t, app.Spec.PodTemplate.Spec.InitContainers, 1) + ensureBucket := app.Spec.PodTemplate.Spec.InitContainers[0] + require.Equal(t, "ensure-bucket", ensureBucket.Name) + requireKafkaContainerSecurityContext(t, ensureBucket.SecurityContext) + require.Len(t, ensureBucket.Args, 4) + require.Contains(t, ensureBucket.Args[0], "head-bucket") + require.Contains(t, ensureBucket.Args[0], "create-bucket") + require.Contains(t, ensureBucket.Args[0], "sleep 2") + require.Contains(t, ensureBucket.Args[0], "-le 150") + require.NotContains(t, ensureBucket.Args[0], testStorage().AccessKey) + require.NotContains(t, ensureBucket.Args[0], testStorage().SecretKey) + require.Equal(t, testStorage().Endpoint, ensureBucket.Args[2]) + require.Equal(t, testStorage().Bucket, ensureBucket.Args[3]) + require.Equal(t, int32(2), *app.Spec.Replicas) + require.Len(t, app.Spec.PodTemplate.Spec.Containers, 1) + + container := app.Spec.PodTemplate.Spec.Containers[0] + require.Equal(t, defaultBufstreamImage, container.Image) + requireKafkaPodSecurityContext(t, app.Spec.PodTemplate.Spec.SecurityContext) + requireKafkaContainerSecurityContext(t, container.SecurityContext) + + envNames := map[string]bool{} + for _, e := range container.Env { + envNames[e.Name] = true + require.NotNil(t, e.ValueFrom, "credentials must come from a secret ref, not inline") + } + require.True(t, envNames[EnvStorageAccessKeyID]) + require.True(t, envNames[EnvStorageSecretAccessKey]) +} + +func TestBringYourOwnObjectStoresDoNotGetBucketInitializer(t *testing.T) { + setOpenShiftMode(t, false) + wandb := testWandb() + nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) + + tests := map[string]objectstore.ConnInfo{ + "custom S3": testStorage(), + "native AWS": {Provider: apiv2.ObjectStoreProviderS3, URI: "s3://bucket", Bucket: "bucket", Region: "us-east-1"}, + "GCS": {Provider: apiv2.ObjectStoreProviderGCS, URI: "gs://bucket", Bucket: "bucket"}, + "Azure": {Provider: apiv2.ObjectStoreProviderAzure, URI: "https://account.blob.core.windows.net/container", Bucket: "container"}, + } + for name, storage := range tests { + t.Run(name, func(t *testing.T) { + app, err := ToBufstreamApplication(wandb, nsn, storage, false, testScheme(t), manifest.Manifest{}) + require.NoError(t, err) + require.Empty(t, app.Spec.PodTemplate.Spec.InitContainers) + }) + } +} + +func TestToBufstreamApplicationDefaultsReplicas(t *testing.T) { + setOpenShiftMode(t, false) + wandb := testWandb() + wandb.Spec.Kafka.ManagedKafka.Replicas = 0 + nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) + + app, err := ToBufstreamApplication(wandb, nsn, testStorage(), true, testScheme(t), manifest.Manifest{}) + require.NoError(t, err) + require.Equal(t, int32(BufstreamReplicas), *app.Spec.Replicas) +} + +func TestApplicationsSecurityContextInOpenShiftMode(t *testing.T) { + setOpenShiftMode(t, true) + wandb := testWandb() + nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) + + // etcd tolerates an arbitrary UID, so it omits fixed IDs for restricted-v2. + etcd, err := ToEtcdApplication(wandb, nsn, testScheme(t), manifest.Manifest{}) + require.NoError(t, err) + requireOpenShiftKafkaPodSecurityContext(t, etcd.Spec.PodTemplate.Spec.SecurityContext) + requireOpenShiftKafkaContainerSecurityContext(t, etcd.Spec.PodTemplate.Spec.Containers[0].SecurityContext) + + // Bufstream keeps its fixed UID even on OpenShift (nonroot-v2 admits it). + bufstream, err := ToBufstreamApplication(wandb, nsn, testStorage(), true, testScheme(t), manifest.Manifest{}) + require.NoError(t, err) + requireKafkaPodSecurityContext(t, bufstream.Spec.PodTemplate.Spec.SecurityContext) + requireKafkaContainerSecurityContext(t, bufstream.Spec.PodTemplate.Spec.Containers[0].SecurityContext) + // The bucket-ensure init container omits a fixed UID and inherits the pod's. + requireOpenShiftKafkaContainerSecurityContext(t, bufstream.Spec.PodTemplate.Spec.InitContainers[0].SecurityContext) +} + +func TestApplicationsUseDedicatedServiceAccount(t *testing.T) { + setOpenShiftMode(t, false) + wandb := testWandb() + nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) + + etcd, err := ToEtcdApplication(wandb, nsn, testScheme(t), manifest.Manifest{}) + require.NoError(t, err) + require.Equal(t, nsn.ServiceAccountName(), etcd.Spec.PodTemplate.Spec.ServiceAccountName) + require.NotNil(t, etcd.Spec.PodTemplate.Spec.AutomountServiceAccountToken) + require.False(t, *etcd.Spec.PodTemplate.Spec.AutomountServiceAccountToken) + + bufstream, err := ToBufstreamApplication(wandb, nsn, testStorage(), true, testScheme(t), manifest.Manifest{}) + require.NoError(t, err) + require.Equal(t, nsn.ServiceAccountName(), bufstream.Spec.PodTemplate.Spec.ServiceAccountName) + require.NotNil(t, bufstream.Spec.PodTemplate.Spec.AutomountServiceAccountToken) + require.False(t, *bufstream.Spec.PodTemplate.Spec.AutomountServiceAccountToken) +} + +func TestToServiceAccount(t *testing.T) { + wandb := testWandb() + nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) + + sa, err := ToServiceAccount(wandb, nsn, testScheme(t)) + require.NoError(t, err) + require.Equal(t, nsn.ServiceAccountName(), sa.Name) + require.Equal(t, "default", sa.Namespace) + require.NotNil(t, sa.AutomountServiceAccountToken) + require.False(t, *sa.AutomountServiceAccountToken) + // Same-namespace resources are owned by the CR for GC. + require.Len(t, sa.OwnerReferences, 1) +} + +func TestToSccRoleBinding(t *testing.T) { + wandb := testWandb() + nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) + + rb, err := ToSccRoleBinding(wandb, nsn, testScheme(t)) + require.NoError(t, err) + require.Equal(t, nsn.SccRoleBindingName(), rb.Name) + require.Equal(t, "default", rb.Namespace) + require.Equal(t, "ClusterRole", rb.RoleRef.Kind) + require.Equal(t, nonRootV2SCCClusterRole, rb.RoleRef.Name) + require.Len(t, rb.Subjects, 1) + require.Equal(t, "ServiceAccount", rb.Subjects[0].Kind) + require.Equal(t, nsn.ServiceAccountName(), rb.Subjects[0].Name) + require.Equal(t, "default", rb.Subjects[0].Namespace) +} + +func TestToCredentialsSecret(t *testing.T) { + wandb := testWandb() + nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) + storage := objectstore.ConnInfo{AccessKey: "ak", SecretKey: "sk"} + + secret, err := ToCredentialsSecret(wandb, nsn, storage, testScheme(t)) + require.NoError(t, err) + require.Equal(t, "ak", secret.StringData[EnvStorageAccessKeyID]) + require.Equal(t, "sk", secret.StringData[EnvStorageSecretAccessKey]) +} + +func requireKafkaPodSecurityContext(t *testing.T, securityContext *corev1.PodSecurityContext) { + t.Helper() + require.NotNil(t, securityContext) + require.NotNil(t, securityContext.RunAsUser) + require.Equal(t, kafkaRunAsUser, *securityContext.RunAsUser) + require.NotNil(t, securityContext.RunAsGroup) + require.Equal(t, kafkaRunAsGroup, *securityContext.RunAsGroup) + require.NotNil(t, securityContext.RunAsNonRoot) + require.True(t, *securityContext.RunAsNonRoot) + require.NotNil(t, securityContext.FSGroup) + require.Equal(t, kafkaFSGroup, *securityContext.FSGroup) + require.NotNil(t, securityContext.FSGroupChangePolicy) + require.Equal(t, corev1.FSGroupChangeOnRootMismatch, *securityContext.FSGroupChangePolicy) + require.NotNil(t, securityContext.SeccompProfile) + require.Equal(t, corev1.SeccompProfileTypeRuntimeDefault, securityContext.SeccompProfile.Type) +} + +func requireKafkaContainerSecurityContext(t *testing.T, securityContext *corev1.SecurityContext) { + t.Helper() + require.NotNil(t, securityContext) + require.NotNil(t, securityContext.RunAsUser) + require.Equal(t, kafkaRunAsUser, *securityContext.RunAsUser) + require.NotNil(t, securityContext.RunAsGroup) + require.Equal(t, kafkaRunAsGroup, *securityContext.RunAsGroup) + require.NotNil(t, securityContext.RunAsNonRoot) + require.True(t, *securityContext.RunAsNonRoot) + require.NotNil(t, securityContext.AllowPrivilegeEscalation) + require.False(t, *securityContext.AllowPrivilegeEscalation) + require.NotNil(t, securityContext.Capabilities) + require.Contains(t, securityContext.Capabilities.Drop, kafkaCapabilityAll) + require.NotNil(t, securityContext.SeccompProfile) + require.Equal(t, corev1.SeccompProfileTypeRuntimeDefault, securityContext.SeccompProfile.Type) +} + +func requireOpenShiftKafkaPodSecurityContext(t *testing.T, securityContext *corev1.PodSecurityContext) { + t.Helper() + require.NotNil(t, securityContext) + require.Nil(t, securityContext.RunAsUser) + require.Nil(t, securityContext.RunAsGroup) + require.Nil(t, securityContext.FSGroup) + require.Nil(t, securityContext.FSGroupChangePolicy) + require.NotNil(t, securityContext.RunAsNonRoot) + require.True(t, *securityContext.RunAsNonRoot) + require.NotNil(t, securityContext.SeccompProfile) + require.Equal(t, corev1.SeccompProfileTypeRuntimeDefault, securityContext.SeccompProfile.Type) +} + +func requireOpenShiftKafkaContainerSecurityContext(t *testing.T, securityContext *corev1.SecurityContext) { + t.Helper() + require.NotNil(t, securityContext) + require.Nil(t, securityContext.RunAsUser) + require.Nil(t, securityContext.RunAsGroup) + require.NotNil(t, securityContext.RunAsNonRoot) + require.True(t, *securityContext.RunAsNonRoot) + require.NotNil(t, securityContext.AllowPrivilegeEscalation) + require.False(t, *securityContext.AllowPrivilegeEscalation) + require.NotNil(t, securityContext.Capabilities) + require.Contains(t, securityContext.Capabilities.Drop, kafkaCapabilityAll) + require.NotNil(t, securityContext.SeccompProfile) + require.Equal(t, corev1.SeccompProfileTypeRuntimeDefault, securityContext.SeccompProfile.Type) +} diff --git a/internal/controller/infra/managed/kafka/bufstream/status.go b/internal/controller/infra/managed/kafka/bufstream/status.go new file mode 100644 index 00000000..69fb12aa --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/status.go @@ -0,0 +1,155 @@ +package bufstream + +import ( + "context" + "time" + + "github.com/samber/lo" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" +) + +func ComputeStatus( + ctx context.Context, + enabled bool, + oldConditions, currentConditions []metav1.Condition, + connection *apiv2.KafkaConnection, + currentGeneration int64, +) (apiv2.KafkaInfraStatus, []corev1.Event, ctrl.Result) { + ctx, _ = logx.WithSlog(ctx, logx.Kafka) + result := apiv2.KafkaInfraStatus{} + + if connection != nil { + result.Connection = *connection + } + + currentConditions = applyDefaultConditions(currentConditions) + + result.Conditions = common.ComputeConditionUpdates( + oldConditions, + currentConditions, + currentGeneration, + common.DefaultConditionExpiry, + ) + + state, events := inferInfraState(ctx, enabled, result.Conditions) + result.State = state + result.Ready = !lo.Contains(common.NotReadyStates, result.State) + + requeueAfter := 3 * time.Minute + switch result.State { + case common.ErrorState: + requeueAfter = 15 * time.Second + case common.DegradedState: + requeueAfter = 5 * time.Minute + case common.PendingState: + requeueAfter = 2 * time.Minute + case common.HealthyState: + requeueAfter = 10 * time.Minute + } + + return result, events, ctrl.Result{RequeueAfter: requeueAfter} +} + +func applyDefaultConditions(conditions []metav1.Condition) []metav1.Condition { + if !common.ContainsType(conditions, KafkaConnectionInfoType) { + conditions = append(conditions, metav1.Condition{ + Type: KafkaConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }) + } + if !common.ContainsType(conditions, KafkaReportedReadyType) { + conditions = append(conditions, metav1.Condition{ + Type: KafkaReportedReadyType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }) + } + return conditions +} + +func inferInfraState( + ctx context.Context, + enabled bool, + conditions []metav1.Condition, +) (string, []corev1.Event) { + if !enabled { + return common.UnavailableState, nil + } + var events []corev1.Event + impliedStates := make(map[string]string, len(conditions)) + + for _, t := range []string{ + ObjectStoreReadyType, + EtcdApplicationType, + BufstreamApplicationType, + KafkaReportedReadyType, + KafkaConnectionInfoType, + } { + impliedStates[t] = inferStateFromCondition(ctx, t, conditions) + } + + hasImpliedState := func(target string) bool { + return len(lo.FilterValues(impliedStates, func(_ string, value string) bool { + return value == target + })) > 0 + } + + if impliedStates[KafkaConnectionInfoType] == common.UnavailableState && + impliedStates[KafkaReportedReadyType] == common.HealthyState { + events = append(events, corev1.Event{ + Type: corev1.EventTypeWarning, + Reason: "KafkaConnectionInfoUnavailable", + Message: "Kafka connection info is unavailable, but Kafka is reported as ready.", + }) + return common.ErrorState, events + } + + stateSignificanceOrder := []string{ + common.ErrorState, + common.UnavailableState, + common.PendingState, + common.DegradedState, + common.HealthyState, + } + for _, s := range stateSignificanceOrder { + if hasImpliedState(s) { + return s, events + } + } + + return common.UnknownState, events +} + +func inferStateFromCondition(ctx context.Context, conditionType string, conditions []metav1.Condition) string { + log := logx.GetSlog(ctx) + cond, found := lo.Find(conditions, func(c metav1.Condition) bool { return c.Type == conditionType }) + if !found { + return common.UnknownState + } + + result := common.UnknownState + if cond.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if cond.Status == metav1.ConditionFalse { + switch cond.Reason { + case common.PendingCreateReason: + result = common.PendingState + case common.PendingDeleteReason: + result = common.UnavailableState + default: + result = common.UnavailableState + } + } + log.Debug( + "implied state", "state", result, "condition", cond.Type, + "reason", cond.Reason, "status", cond.Status, + ) + return result +} diff --git a/internal/controller/infra/managed/kafka/bufstream/values.go b/internal/controller/infra/managed/kafka/bufstream/values.go new file mode 100644 index 00000000..0d12e4d8 --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/values.go @@ -0,0 +1,82 @@ +package bufstream + +const ( + KafkaModuleName = "kafka" + + // TODO: remove these hardcoded defaults once all supported manifest versions + // supply kafka.image and kafka.images.{etcd,bucketEnsure}. + + // defaultBufstreamImage is the Bufstream broker image. Brokers are stateless; + // all data lives in object storage and metadata in etcd. Buf publishes images + // to this public OCI registry (Docker Hub mirrors are no longer maintained). + defaultBufstreamImage = "us-docker.pkg.dev/buf-images-1/buf/images/bufstream:0.4.15" + + // defaultEtcdImage is the metadata store image. Uses the official upstream + // etcd image, which is configured via native ETCD_* environment variables. + defaultEtcdImage = "quay.io/coreos/etcd:v3.5.31" + + defaultBucketEnsureImage = "amazon/aws-cli:2.35.10" + + // Kafka-compatible listener exposed by Bufstream. + KafkaListenerPort = 9092 + // Admin RPC interface. + AdminPort = 9089 + // Debug interface that also serves Prometheus metrics. + DebugPort = 9090 + + // etcd client port. + EtcdClientPort = 2379 + // etcd peer port. + EtcdPeerPort = 2380 + + // EtcdReplicas is the size of the etcd cluster. etcd requires an odd member + // count for quorum; three tolerates a single member failure, which is the + // minimum viable topology for production. It is intentionally not + // customer-configurable. + EtcdReplicas = 3 + + // BufstreamReplicas is the default number of stateless Bufstream brokers. + // Two brokers tolerate a single broker (or node) failure without an outage; + // because all durable state lives in object storage and etcd, brokers can be + // scaled freely. Used when no explicit replica count is configured. + BufstreamReplicas = 2 + + // EtcdClusterToken namespaces the etcd cluster's bootstrap so that distinct + // W&B installs cannot accidentally cross-join. + EtcdClusterToken = "wandb-etcd" + + // Mount point for the rendered bufstream.yaml. + ConfigMountPath = "/etc/bufstream" + ConfigFileName = "bufstream.yaml" + + // etcd data directory and PVC template name. + EtcdDataDir = "/etcd-data" + EtcdDataVolumeName = "data" + + // Env vars used to inject object-store credentials into the broker. Names are + // provider-neutral because only one provider is active per deployment and the + // Bufstream config simply references whichever env var holds the value. + EnvStorageAccessKeyID = "BUFSTREAM_STORAGE_ACCESS_KEY_ID" + EnvStorageSecretAccessKey = "BUFSTREAM_STORAGE_SECRET_ACCESS_KEY" +) + +const ( + // Condition types surfaced on the Kafka infra status. + EtcdApplicationType = "EtcdApplication" + BufstreamApplicationType = "BufstreamApplication" + KafkaConnectionInfoType = "KafkaConnectionInfo" + KafkaReportedReadyType = "KafkaReportedReady" + ObjectStoreReadyType = "ObjectStoreReady" +) + +const ( + ApplicationResourceType = "Application" + ConfigMapResourceType = "ConfigMap" + SecretResourceType = "Secret" + ServiceAccountResourceType = "ServiceAccount" + RoleBindingResourceType = "RoleBinding" + AppConnTypeName = "KafkaAppConn" +) + +// nonRootV2SCCClusterRole is the auto-generated ClusterRole for nonroot-v2. +const nonRootV2SCCClusterRole = "system:openshift:scc:nonroot-v2" diff --git a/internal/controller/infra/managed/kafka/bufstream/write.go b/internal/controller/infra/managed/kafka/bufstream/write.go new file mode 100644 index 00000000..6cdad35a --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/write.go @@ -0,0 +1,236 @@ +package bufstream + +import ( + "context" + "strconv" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/external/objectstore" + "github.com/wandb/operator/internal/logx" + "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// WriteState reconciles the managed Bufstream resources: an etcd StatefulSet, a +// credentials secret and config map, and the Bufstream broker Deployment. It +// gates on object store readiness because Bufstream needs an S3 bucket. +func WriteState( + ctx context.Context, + cl client.Client, + wandb *apiv2.WeightsAndBiases, + mfst manifest.Manifest, +) []metav1.Condition { + ctx, log := logx.WithSlog(ctx, logx.Kafka) + + spec := wandb.Spec.Kafka.ManagedKafka + nsnBuilder := CreateNsNameBuilder(types.NamespacedName{Namespace: spec.Namespace, Name: spec.Name}) + + storage, ready, err := resolveStorage(ctx, cl, wandb, spec) + if err != nil { + log.Error("failed to resolve object store connection for bufstream", logx.ErrAttr(err)) + return []metav1.Condition{ + {Type: common.ReconciledType, Status: metav1.ConditionFalse, Reason: common.ApiErrorReason}, + {Type: ObjectStoreReadyType, Status: metav1.ConditionUnknown, Reason: common.ApiErrorReason}, + } + } + if !ready { + log.Info("object store not ready yet, deferring bufstream provisioning") + return []metav1.Condition{ + {Type: ObjectStoreReadyType, Status: metav1.ConditionFalse, Reason: common.PendingCreateReason}, + } + } + //objectStoreSpec, _ := apiv2.ResolveInstance(wandb.Spec.ObjectStore, bufstreamObjectStoreInstance) + ensureBucket := false + + credsSecret, err := ToCredentialsSecret(wandb, nsnBuilder, storage, cl.Scheme()) + if err != nil { + return translateError(err) + } + configMap, err := ToConfigMap(wandb, nsnBuilder, storage, cl.Scheme()) + if err != nil { + return translateError(err) + } + serviceAccount, err := ToServiceAccount(wandb, nsnBuilder, cl.Scheme()) + if err != nil { + return translateError(err) + } + // On OpenShift, bind the SA to nonroot-v2 so broker runs as its fixed UID. + var sccRoleBinding *rbacv1.RoleBinding + if utils.IsOpenShift() { + sccRoleBinding, err = ToSccRoleBinding(wandb, nsnBuilder, cl.Scheme()) + if err != nil { + return translateError(err) + } + } + etcdApp, err := ToEtcdApplication(wandb, nsnBuilder, cl.Scheme(), mfst) + if err != nil { + return translateError(err) + } + bufstreamApp, err := ToBufstreamApplication(wandb, nsnBuilder, storage, ensureBucket, cl.Scheme(), mfst) + if err != nil { + return translateError(err) + } + + results := []metav1.Condition{ + {Type: ObjectStoreReadyType, Status: metav1.ConditionTrue, Reason: common.ResourceExistsReason}, + } + results = append(results, writeResource(ctx, cl, common.ReconciledType, SecretResourceType, credsSecret, &corev1.Secret{})...) + results = append(results, writeResource(ctx, cl, common.ReconciledType, ConfigMapResourceType, configMap, &corev1.ConfigMap{})...) + results = append(results, writeResource(ctx, cl, common.ReconciledType, ServiceAccountResourceType, serviceAccount, &corev1.ServiceAccount{})...) + if sccRoleBinding != nil { + results = append(results, writeResource(ctx, cl, common.ReconciledType, RoleBindingResourceType, sccRoleBinding, &rbacv1.RoleBinding{})...) + } + results = append(results, writeResource(ctx, cl, EtcdApplicationType, ApplicationResourceType, etcdApp, &apiv2.Application{})...) + results = append(results, writeResource(ctx, cl, BufstreamApplicationType, ApplicationResourceType, bufstreamApp, &apiv2.Application{})...) + + return results +} + +func translateError(err error) []metav1.Condition { + return []metav1.Condition{ + {Type: common.ReconciledType, Status: metav1.ConditionFalse, Reason: common.ControllerErrorReason, Message: err.Error()}, + } +} + +// writeResource gets the current object, runs the generic CRUD, and maps the +// resulting action onto a status condition of the given type. +func writeResource[T client.Object]( + ctx context.Context, + cl client.Client, + conditionType string, + resourceType string, + desired T, + actual T, +) []metav1.Condition { + found, err := common.GetResource(ctx, cl, client.ObjectKeyFromObject(desired), resourceType, actual) + if err != nil { + return []metav1.Condition{ + {Type: common.ReconciledType, Status: metav1.ConditionFalse, Reason: common.ApiErrorReason}, + {Type: conditionType, Status: metav1.ConditionUnknown, Reason: common.ApiErrorReason}, + } + } + if !found { + var zero T + actual = zero + } + + action, err := common.CrudResource(ctx, cl, desired, actual) + if err != nil { + return []metav1.Condition{ + {Type: common.ReconciledType, Status: metav1.ConditionFalse, Reason: common.ApiErrorReason}, + } + } + + if conditionType == common.ReconciledType { + // Supporting resources only report reconcile success/failure. + return nil + } + + return []metav1.Condition{actionToCondition(conditionType, action)} +} + +func actionToCondition(conditionType string, action common.CrudAction) metav1.Condition { + switch action { + case common.CreateAction: + return metav1.Condition{Type: conditionType, Status: metav1.ConditionFalse, Reason: common.PendingCreateReason} + case common.DeleteAction: + return metav1.Condition{Type: conditionType, Status: metav1.ConditionFalse, Reason: common.PendingDeleteReason} + case common.UpdateAction, common.UnchangedAction: + return metav1.Condition{Type: conditionType, Status: metav1.ConditionTrue, Reason: common.ResourceExistsReason} + default: + return metav1.Condition{Type: conditionType, Status: metav1.ConditionFalse, Reason: common.NoResourceReason} + } +} + +// bufstreamObjectStoreInstance is the object-store instance name Bufstream +// prefers for message storage; ResolveInstance falls back to the default +// instance when it is not provisioned. +const bufstreamObjectStoreInstance = "bufstream" + +// resolveStorage reads the object store connection secret and parses its +// connection string into the provider-specific values needed to configure +// Bufstream. Returns ready=false when the object store is not yet available. +func resolveStorage( + ctx context.Context, + cl client.Client, + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedKafkaSpec, +) (objectstore.ConnInfo, bool, error) { + status, ok := apiv2.ResolveInstance(wandb.Status.ObjectStoreStatus, bufstreamObjectStoreInstance) + if !ok || !status.Ready { + return objectstore.ConnInfo{}, false, nil + } + + resolver := &utils.ConnSecretResolver{Client: cl, Namespace: spec.Namespace, Cache: map[string]*corev1.Secret{}} + + connInfo := objectstore.ConnInfo{} + + provider, err := resolver.Value(ctx, status.Connection.Provider) + if err != nil { + return objectstore.ConnInfo{}, true, err + } + connInfo.Provider = apiv2.ObjectStoreProvider(provider) + + connInfo.Bucket, err = resolver.Value(ctx, status.Connection.Bucket) + if err != nil { + return objectstore.ConnInfo{}, true, err + } + + connInfo.Endpoint, err = resolver.Value(ctx, status.Connection.Endpoint) + if err != nil { + return objectstore.ConnInfo{}, true, err + } + + connInfo.Port, err = resolver.Value(ctx, status.Connection.Port) + if err != nil { + return objectstore.ConnInfo{}, true, err + } + + connInfo.Region, err = resolver.Value(ctx, status.Connection.Region) + if err != nil { + return objectstore.ConnInfo{}, true, err + } + + connInfo.AccessKey, err = resolver.Value(ctx, status.Connection.AccessKey) + if err != nil { + return objectstore.ConnInfo{}, true, err + } + + connInfo.SecretKey, err = resolver.Value(ctx, status.Connection.SecretKey) + if err != nil { + return objectstore.ConnInfo{}, true, err + } + + forcePathStyleString, err := resolver.Value(ctx, status.Connection.ForcePathStyle) + if err != nil { + return objectstore.ConnInfo{}, true, err + } + if fps, parseErr := strconv.ParseBool(forcePathStyleString); parseErr == nil { + connInfo.ForcePathStyle = fps + } else { + // Connection secrets written before the operator derived this key lack it. + connInfo.ForcePathStyle = objectstore.RequiresPathStyle(connInfo.Endpoint) + } + + tlsEnabledString, err := resolver.Value(ctx, status.Connection.TlsEnabled) + if err != nil { + return objectstore.ConnInfo{}, true, err + } + connInfo.TlsEnabled, err = strconv.ParseBool(tlsEnabledString) + if err != nil { + connInfo.TlsEnabled = false + } + + connInfo.Path, err = resolver.Value(ctx, status.Connection.Path) + if err != nil { + return objectstore.ConnInfo{}, true, err + } + + return connInfo, true, nil +} diff --git a/internal/controller/infra/managed/kafka/bufstream/write_test.go b/internal/controller/infra/managed/kafka/bufstream/write_test.go new file mode 100644 index 00000000..c1b78b03 --- /dev/null +++ b/internal/controller/infra/managed/kafka/bufstream/write_test.go @@ -0,0 +1,102 @@ +package bufstream + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + apiv2 "github.com/wandb/operator/api/v2" +) + +// resolveStorageFixture seeds a connection secret with the given keys and a +// wandb whose default object-store status selects every key from it. +func resolveStorageFixture(t *testing.T, data map[string]string) (ctrlclient.Client, *apiv2.WeightsAndBiases, *apiv2.ManagedKafkaSpec) { + t.Helper() + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + require.NoError(t, apiv2.AddToScheme(scheme)) + + secretData := map[string][]byte{} + for k, v := range data { + secretData[k] = []byte(v) + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb-objectstore-connection", Namespace: "wandb"}, + Data: secretData, + } + + sel := func(key string) corev1.SecretKeySelector { + return corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: secret.Name}, + Key: key, + } + } + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "wandb"}, + Status: apiv2.WeightsAndBiasesStatus{ + ObjectStoreStatus: map[string]apiv2.ObjectStoreInfraStatus{ + apiv2.DefaultInstanceName: { + WBInfraStatus: apiv2.WBInfraStatus{Ready: true}, + Connection: apiv2.ObjectStoreConnection{ + Provider: sel("Provider"), + Bucket: sel("Bucket"), + Endpoint: sel("Host"), + Port: sel("Port"), + Region: sel("Region"), + AccessKey: sel("AccessKey"), + SecretKey: sel("SecretKey"), + ForcePathStyle: sel("ForcePathStyle"), + TlsEnabled: sel("TlsEnabled"), + }, + }, + }, + }, + } + + cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + return cl, wandb, &apiv2.ManagedKafkaSpec{Namespace: "wandb"} +} + +func TestResolveStorage_ForcePathStyleFallbackOnMissingKey(t *testing.T) { + cl, wandb, spec := resolveStorageFixture(t, map[string]string{ + "Provider": "s3", + "Bucket": "wandb", + "Host": "minio.wandb.localhost", + "Port": "8080", + }) + + info, ready, err := resolveStorage(context.Background(), cl, wandb, spec) + require.NoError(t, err) + require.True(t, ready) + require.True(t, info.ForcePathStyle, "secrets predating the derived key must fall back to the endpoint rule") +} + +func TestResolveStorage_ForcePathStyleExplicitRespected(t *testing.T) { + cl, wandb, spec := resolveStorageFixture(t, map[string]string{ + "Provider": "s3", + "Bucket": "wandb", + "Host": "minio.wandb.localhost", + "ForcePathStyle": "false", + }) + + info, _, err := resolveStorage(context.Background(), cl, wandb, spec) + require.NoError(t, err) + require.False(t, info.ForcePathStyle) +} + +func TestResolveStorage_NoEndpointStaysVirtualHosted(t *testing.T) { + cl, wandb, spec := resolveStorageFixture(t, map[string]string{ + "Provider": "s3", + "Bucket": "wandb", + }) + + info, _, err := resolveStorage(context.Background(), cl, wandb, spec) + require.NoError(t, err) + require.False(t, info.ForcePathStyle, "native AWS S3 must not force path-style") +} diff --git a/internal/controller/infra/managed/mysql/moco/conn.go b/internal/controller/infra/managed/mysql/moco/conn.go new file mode 100644 index 00000000..918a3b1a --- /dev/null +++ b/internal/controller/infra/managed/mysql/moco/conn.go @@ -0,0 +1,103 @@ +package moco + +import ( + "context" + "errors" + "fmt" + "net/url" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type mysqlConnInfo struct { + Host string + Port string + User string + Password string + Database string +} + +func (c *mysqlConnInfo) toURL() string { + password := url.QueryEscape(c.Password) + return fmt.Sprintf("mysql://%s:%s@%s:%s/%s", c.User, password, c.Host, c.Port, c.Database) +} + +func writeMySQLConnInfo( + ctx context.Context, + client client.Client, + owner client.Object, + nsnBuilder *NsNameBuilder, + connInfo *mysqlConnInfo, +) ( + *apiv2.MysqlConnection, error, +) { + var err error + var found bool + var gvk schema.GroupVersionKind + var actual = &corev1.Secret{} + + if connInfo == nil { + return nil, errors.New("missing connection info") + } + + nsName := nsnBuilder.ConnectionNsName() + urlKey := "url" + + if found, err = common.GetResource( + ctx, client, nsName, AppConnTypeName, actual, + ); err != nil { + return nil, err + } + if !found { + actual = nil + } + + if gvk, err = client.GroupVersionKindFor(owner); err != nil { + return nil, fmt.Errorf("could not get GVK for owner: %w", err) + } + ref := metav1.OwnerReference{ + APIVersion: gvk.GroupVersion().String(), + Kind: gvk.Kind, + Name: owner.GetName(), + UID: owner.GetUID(), + Controller: ptr.To(false), + BlockOwnerDeletion: ptr.To(false), + } + + desired := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsName.Name, + Namespace: nsName.Namespace, + OwnerReferences: []metav1.OwnerReference{ref}, + }, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{ + urlKey: connInfo.toURL(), + "Host": connInfo.Host, + "Port": connInfo.Port, + "Database": connInfo.Database, + "Username": connInfo.User, + "Password": connInfo.Password, + }, + } + + if _, err = common.CrudResource(ctx, client, desired, actual); err != nil { + return nil, err + } + + localRef := corev1.LocalObjectReference{Name: nsName.Name} + return &apiv2.MysqlConnection{ + URL: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: urlKey, Optional: ptr.To(false)}, + Host: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Host", Optional: ptr.To(false)}, + Port: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Port", Optional: ptr.To(false)}, + Database: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Database", Optional: ptr.To(false)}, + Username: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Username", Optional: ptr.To(false)}, + Password: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Password", Optional: ptr.To(false)}, + }, nil +} diff --git a/internal/controller/infra/managed/mysql/moco/detach.go b/internal/controller/infra/managed/mysql/moco/detach.go new file mode 100644 index 00000000..03d4c8b3 --- /dev/null +++ b/internal/controller/infra/managed/mysql/moco/detach.go @@ -0,0 +1,99 @@ +package moco + +import ( + "context" + "fmt" + + mocov1beta2 "github.com/cybozu-go/moco/api/v1beta2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func CheckDetached( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + wandbUID types.UID, + desiredReplicas int32, +) []metav1.Condition { + nsnBuilder := createNsNameBuilder(specNamespacedName) + actual := &mocov1beta2.MySQLCluster{} + found, err := common.GetResource(ctx, cl, nsnBuilder.ClusterNsName(), ResourceTypeName, actual) + if err != nil || !found { + return nil + } + if !common.IsDetached(actual, wandbUID) { + return nil + } + + if desiredReplicas > 0 && actual.Spec.Replicas != desiredReplicas { + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.DetachedSpecMismatch, + Message: fmt.Sprintf("detached MySQL CR spec mismatch: replicas want %d, have %d", desiredReplicas, actual.Spec.Replicas), + }, + } + } + return nil +} + +func DetachFinalizer( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + wandbOwner client.Object, +) error { + ctx, log := logx.WithSlog(ctx, logx.Mysql) + nsnBuilder := createNsNameBuilder(specNamespacedName) + + var actual = &mocov1beta2.MySQLCluster{} + found, err := common.GetResource(ctx, cl, nsnBuilder.ClusterNsName(), ResourceTypeName, actual) + if err != nil { + return err + } + if !found { + log.Info("abort detach finalizer: MysqlCluster CR not found") + return nil + } + + if common.IsDetached(actual, wandbOwner.GetUID()) { + log.Debug("MysqlCluster CR already detached") + return nil + } + + common.RemoveOwnerReference(actual, wandbOwner.GetUID()) + if err = cl.Update(ctx, actual); err != nil { + if errors.IsNotFound(err) { + return nil + } + log.Error("error detaching MysqlCluster CR", logx.ErrAttr(err)) + return err + } + log.Info("detached MysqlCluster CR", "name", actual.Name) + + if err = detachConnectionSecret(ctx, cl, nsnBuilder, wandbOwner); err != nil { + log.Error("error detaching connection secret", logx.ErrAttr(err)) + return err + } + return nil +} + +func detachConnectionSecret(ctx context.Context, cl client.Client, nsnBuilder *NsNameBuilder, wandbOwner client.Object) error { + secret := &corev1.Secret{} + found, err := common.GetResource(ctx, cl, nsnBuilder.ConnectionNsName(), "Secret", secret) + if err != nil || !found { + return err + } + common.RemoveOwnerReference(secret, wandbOwner.GetUID()) + if err = cl.Update(ctx, secret); err != nil && !errors.IsNotFound(err) { + return err + } + return nil +} diff --git a/internal/controller/infra/managed/mysql/moco/image_test.go b/internal/controller/infra/managed/mysql/moco/image_test.go new file mode 100644 index 00000000..f642a0f0 --- /dev/null +++ b/internal/controller/infra/managed/mysql/moco/image_test.go @@ -0,0 +1,31 @@ +package moco + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/wandb/operator/pkg/wandb/manifest" +) + +var _ = Describe("MocoMySQLImage", func() { + DescribeTable("resolves the image string", + func(img manifest.ImageRef, globalImageRegistry, expected string) { + Expect(MocoMySQLImage(img, globalImageRegistry)).To(Equal(expected)) + }, + + // 1) No image from the manifest: fall back to the hardcoded default. + Entry("no manifest image", manifest.ImageRef{}, "", defaultMocoMySQLImage), + + // 2) Manifest supplies the image, no global registry: use it verbatim. + Entry("manifest image", + manifest.ImageRef{Registry: "ghcr.io", Repository: "cybozu-go/moco/mysql", Tag: "8.4.8"}, + "", + "ghcr.io/cybozu-go/moco/mysql:8.4.8"), + + // 3) Global image registry is prepended in front of the manifest image. + Entry("manifest image, global registry set", + manifest.ImageRef{Registry: "ghcr.io", Repository: "cybozu-go/moco/mysql", Tag: "8.4.8"}, + "myregistry.io", + "myregistry.io/ghcr.io/cybozu-go/moco/mysql:8.4.8"), + ) +}) diff --git a/internal/controller/infra/managed/mysql/moco/mysql_suite_test.go b/internal/controller/infra/managed/mysql/moco/mysql_suite_test.go new file mode 100644 index 00000000..055cb199 --- /dev/null +++ b/internal/controller/infra/managed/mysql/moco/mysql_suite_test.go @@ -0,0 +1,13 @@ +package moco + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMysql(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "MySQL Suite") +} diff --git a/internal/controller/infra/managed/mysql/moco/naming.go b/internal/controller/infra/managed/mysql/moco/naming.go new file mode 100644 index 00000000..86409661 --- /dev/null +++ b/internal/controller/infra/managed/mysql/moco/naming.go @@ -0,0 +1,67 @@ +package moco + +import ( + "fmt" + + "github.com/wandb/operator/internal/controller/common" + "k8s.io/apimachinery/pkg/types" +) + +// MaxClusterNameLength mirrors Moco's admission cap on MySQLCluster names. +const MaxClusterNameLength = 40 + +const defaultNameSuffix = "-mysql" + +// DefaultSpecName derives the managed MySQL name for a CR instance, shortened +// to Moco's cap. +func DefaultSpecName(crName, instanceKey string) string { + return common.FitDefaultInfraName(common.InstanceBaseName(crName, instanceKey), defaultNameSuffix, MaxClusterNameLength) +} + +type NsNameBuilder struct { + baseNsName types.NamespacedName +} + +func CreateNsNameBuilder(baseNsName types.NamespacedName) *NsNameBuilder { + return &NsNameBuilder{ + baseNsName: baseNsName, + } +} + +func (n *NsNameBuilder) Namespace() string { + return n.baseNsName.Namespace +} + +func (n *NsNameBuilder) SpecName() string { + return n.baseNsName.Name +} + +func (n *NsNameBuilder) ClusterName() string { + return n.SpecName() +} + +func (n *NsNameBuilder) ClusterNsName() types.NamespacedName { + return types.NamespacedName{ + Namespace: n.Namespace(), + Name: n.ClusterName(), + } +} + +func (n *NsNameBuilder) ConnectionName() string { + return fmt.Sprintf("%s-connection", n.SpecName()) +} + +func (n *NsNameBuilder) ConnectionNsName() types.NamespacedName { + return types.NamespacedName{ + Namespace: n.Namespace(), + Name: n.ConnectionName(), + } +} + +func createNsNameBuilder(baseNsName types.NamespacedName) *NsNameBuilder { + return CreateNsNameBuilder(baseNsName) +} + +func MyCnfConfigMapName(specName string) string { + return specName + "-mycnf" +} diff --git a/internal/controller/infra/managed/mysql/moco/purge.go b/internal/controller/infra/managed/mysql/moco/purge.go new file mode 100644 index 00000000..0bd0ba84 --- /dev/null +++ b/internal/controller/infra/managed/mysql/moco/purge.go @@ -0,0 +1,85 @@ +package moco + +import ( + "context" + + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func PurgeFinalizer( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + onDeleteRule common.OnDeleteRule, +) error { + ctx, _ = logx.WithSlog(ctx, logx.Mysql) + if onDeleteRule.Policy != common.Purge { + return nil + } + return purgeAssociatedResources(ctx, cl, specNamespacedName.Namespace, onDeleteRule.Selector) +} + +func purgeAssociatedResources( + ctx context.Context, + cl client.Client, + namespace string, + onDeleteSelector labels.Selector, +) error { + log := logx.GetSlog(ctx) + listOptions := &client.ListOptions{ + Namespace: namespace, + LabelSelector: onDeleteSelector, + } + + // PVCs + pvcList := &corev1.PersistentVolumeClaimList{} + if err := cl.List(ctx, pvcList, listOptions); err != nil { + return err + } + if len(pvcList.Items) > 0 { + log.Info( + "Purging associated PVCs", + "count", len(pvcList.Items), "selector", onDeleteSelector.String(), + ) + } else { + log.Debug( + "No associated PVCs found to purge", + "selector", onDeleteSelector.String(), + ) + } + for _, pvc := range pvcList.Items { + if err := cl.Delete(ctx, &pvc); err != nil && !errors.IsNotFound(err) { + return err + } + } + + // Secrets + secretList := &corev1.SecretList{} + if err := cl.List(ctx, secretList, listOptions); err != nil { + return err + } + if len(secretList.Items) > 0 { + log.Info( + "Purging associated Secrets", + "count", len(secretList.Items), "selector", onDeleteSelector.String(), + ) + } else { + log.Debug( + "No associated Secrets found to purge", + "selector", onDeleteSelector.String(), + ) + } + for _, secret := range secretList.Items { + if err := cl.Delete(ctx, &secret); err != nil && !errors.IsNotFound(err) { + return err + } + } + + return nil +} diff --git a/internal/controller/infra/managed/mysql/moco/read.go b/internal/controller/infra/managed/mysql/moco/read.go new file mode 100644 index 00000000..30aa87cd --- /dev/null +++ b/internal/controller/infra/managed/mysql/moco/read.go @@ -0,0 +1,150 @@ +package moco + +import ( + "context" + "fmt" + + mocov1beta2 "github.com/cybozu-go/moco/api/v1beta2" + apiv2 "github.com/wandb/operator/api/v2" + ctrlcommon "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func readConnectionDetails(ctx context.Context, c client.Client, actual *mocov1beta2.MySQLCluster, nn types.NamespacedName) *mysqlConnInfo { + log := logx.GetSlog(ctx) + + cred := &corev1.Secret{} + secretName := "moco-" + nn.Name + if err := c.Get(ctx, types.NamespacedName{Name: secretName, Namespace: nn.Namespace}, cred); err != nil { + log.Error("Failed to get Moco credentials", logx.ErrAttr(err), "Secret", secretName) + return nil + } + pw := string(cred.Data["WRITABLE_PASSWORD"]) + if pw == "" { + return nil + } + return &mysqlConnInfo{ + Host: fmt.Sprintf("moco-%s-primary.%s.svc.cluster.local", actual.Name, actual.Namespace), + Port: "3306", + User: "moco-writable", + Database: "wandb_local", + Password: pw, + } +} + +func ReadState( + ctx context.Context, + k8sClient client.Client, + specNamespacedName types.NamespacedName, + wandbOwner client.Object, + onDeleteRule ctrlcommon.OnDeleteRule, +) ([]metav1.Condition, *apiv2.MysqlConnection) { + ctx, _ = logx.WithSlog(ctx, logx.Mysql) + log := logx.GetSlog(ctx) + + var actual = &mocov1beta2.MySQLCluster{} + conditions := make([]metav1.Condition, 0) + nsnBuilder := createNsNameBuilder(specNamespacedName) + + found, err := ctrlcommon.GetResource( + ctx, k8sClient, nsnBuilder.ClusterNsName(), ResourceTypeName, actual, + ) + if err != nil { + return []metav1.Condition{ + { + Type: MySQLCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + }, nil + } + if !found { + actual = nil + if onDeleteRule.Policy == ctrlcommon.Purge { + log.Debug( + "Attempting to purge associated moco resources after deletion", + "tenantName", nsnBuilder.ClusterName(), + ) + err = purgeAssociatedResources(ctx, k8sClient, specNamespacedName.Namespace, onDeleteRule.Selector) + if err != nil { + conditions = append( + conditions, + metav1.Condition{ + Type: MySQLCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + ) + } else { + conditions = append(conditions, metav1.Condition{ + Type: MySQLCustomResourceType, + Status: metav1.ConditionFalse, + Reason: ctrlcommon.PendingDeleteReason, + }, + ) + } + } + } + + var connection *apiv2.MysqlConnection + + if actual != nil { + connInfo := readConnectionDetails(ctx, k8sClient, actual, specNamespacedName) + + connection, err = writeMySQLConnInfo( + ctx, k8sClient, wandbOwner, nsnBuilder, connInfo, + ) + if err != nil { + if err.Error() == "missing connection info" { + return []metav1.Condition{ + { + Type: MySQLConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: ctrlcommon.NoResourceReason, + }, + }, nil + } + return []metav1.Condition{ + { + Type: MySQLConnectionInfoType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + }, nil + } + if connection == nil { + conditions = append(conditions, metav1.Condition{ + Type: MySQLConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: ctrlcommon.NoResourceReason, + }) + } else { + conditions = append(conditions, metav1.Condition{ + Type: MySQLConnectionInfoType, + Status: metav1.ConditionTrue, + Reason: ctrlcommon.ResourceExistsReason, + }) + } + + conditions = append(conditions, computeMySQLReportedReadyCondition(ctx, actual)...) + } + + return conditions, connection +} + +func computeMySQLReportedReadyCondition(_ context.Context, clusterCR *mocov1beta2.MySQLCluster) []metav1.Condition { + if clusterCR == nil { + return []metav1.Condition{} + } + for _, c := range clusterCR.Status.Conditions { + if c.Type == "Healthy" && c.Status == metav1.ConditionTrue { + return []metav1.Condition{{Type: MySQLReportedReadyType, Status: metav1.ConditionTrue, Reason: "Online"}} + } + } + return []metav1.Condition{{Type: MySQLReportedReadyType, Status: metav1.ConditionFalse, Reason: "NotReady"}} + +} diff --git a/internal/controller/infra/managed/mysql/moco/spec.go b/internal/controller/infra/managed/mysql/moco/spec.go new file mode 100644 index 00000000..89c88c48 --- /dev/null +++ b/internal/controller/infra/managed/mysql/moco/spec.go @@ -0,0 +1,197 @@ +package moco + +import ( + "context" + + mocov1beta2 "github.com/cybozu-go/moco/api/v1beta2" + mococonstants "github.com/cybozu-go/moco/pkg/constants" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + corev1ac "k8s.io/client-go/applyconfigurations/core/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + MysqlModuleName = "moco" + + // Moco names the resulting PVCs "<dataVolumeName>-<cluster.PrefixedName()>-<ordinal>" + // (= "<dataVolumeName>-moco-<cluster>-<n>"); ensurePVCLabels and purge rely on this. + dataVolumeName = mococonstants.MySQLDataVolumeName + + // TODO: remove this hardcoded default once all supported manifest versions + // supply mysql.<instance>.images.mysql. + defaultMocoMySQLImage = "ghcr.io/cybozu-go/moco/mysql:8.4.8" +) + +func MocoMySQLImage(img manifest.ImageRef, globalImageRegistry string) string { + if out := img.GetImage(globalImageRegistry); out != "" { + return out + } + // Fallback for older manifests that don't supply the image. + return defaultMocoMySQLImage +} + +const ( + mocoMySQLRunAsUser int64 = mococonstants.ContainerUID + mocoMySQLRunAsGroup int64 = mococonstants.ContainerGID + mocoMySQLFSGroup int64 = mococonstants.ContainerGID + + mocoMySQLCapabilityAll corev1.Capability = "ALL" +) + +func ToMocoMySQLClusterSpec( + ctx context.Context, + spec apiv2.ManagedMysqlSpec, + wandb *apiv2.WeightsAndBiases, + scheme *runtime.Scheme, + mfst manifest.Manifest, +) (*mocov1beta2.MySQLCluster, *corev1.ConfigMap, error) { + + replicas := spec.Replicas + + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: MyCnfConfigMapName(spec.Name), + Namespace: spec.Namespace, + Labels: BuildWandbMysqlLabels(wandb), + }, + Data: map[string]string{ + "sync_binlog": "1", + "innodb_flush_log_at_trx_commit": "1", + }, + } + if err := controllerutil.SetControllerReference(wandb, cm, scheme); err != nil { + return nil, nil, err + } + + cluster := &mocov1beta2.MySQLCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: spec.Name, Namespace: spec.Namespace, + Labels: BuildWandbMysqlLabels(wandb), + }, + Spec: mocov1beta2.MySQLClusterSpec{ + Replicas: replicas, + MySQLConfigMapName: ptr.To(MyCnfConfigMapName(spec.Name)), + PodTemplate: mocov1beta2.PodTemplateSpec{ + Spec: buildMocoPodSpec(spec.Config.Resources, mfst.Mysql["default"].Images["mysql"], wandb), + OverwriteContainers: mocoOverwriteContainers(), + }, + VolumeClaimTemplates: []mocov1beta2.PersistentVolumeClaim{ + { + ObjectMeta: mocov1beta2.ObjectMeta{Name: dataVolumeName}, + Spec: buildPVCSpec(spec.StorageSize), + }, + }, + }, + } + + // A non-empty Collectors list makes MOCO inject the mysqld_exporter sidecar. + if spec.Telemetry.Enabled { + cluster.Spec.Collectors = []string{"engine_innodb_status", "info_schema.innodb_metrics"} + } + + if err := controllerutil.SetControllerReference(wandb, cluster, scheme); err != nil { + return nil, nil, err + } + return cluster, cm, nil +} + +func buildMocoPodSpec(resources corev1.ResourceRequirements, img manifest.ImageRef, wandb *apiv2.WeightsAndBiases,) mocov1beta2.PodSpecApplyConfiguration { + container := corev1ac.Container(). + WithName("mysqld"). + WithImage(MocoMySQLImage(img, wandb.Spec.Global.ImageRegistry)). + WithSecurityContext(mocoContainerSecurityContext()) + + if resources.Requests != nil || resources.Limits != nil { + container = container.WithResources( + corev1ac.ResourceRequirements(). + WithRequests(resources.Requests). + WithLimits(resources.Limits), + ) + } + + podSpec := corev1ac.PodSpec(). + WithSecurityContext(mocoPodSecurityContext()). + WithContainers(container) + return mocov1beta2.PodSpecApplyConfiguration(*podSpec) +} + +func mocoPodSecurityContext() *corev1ac.PodSecurityContextApplyConfiguration { + securityContext := corev1ac.PodSecurityContext(). + WithRunAsNonRoot(true). + WithSeccompProfile(corev1ac.SeccompProfile(). + WithType(corev1.SeccompProfileTypeRuntimeDefault)) + if !utils.IsOpenShift() { + securityContext = securityContext. + WithRunAsUser(mocoMySQLRunAsUser). + WithRunAsGroup(mocoMySQLRunAsGroup). + WithFSGroup(mocoMySQLFSGroup). + WithFSGroupChangePolicy(corev1.FSGroupChangeOnRootMismatch) + } + return securityContext +} + +func mocoContainerSecurityContext() *corev1ac.SecurityContextApplyConfiguration { + securityContext := corev1ac.SecurityContext(). + WithRunAsNonRoot(true). + WithAllowPrivilegeEscalation(false). + WithCapabilities(corev1ac.Capabilities().WithDrop(mocoMySQLCapabilityAll)). + WithSeccompProfile(corev1ac.SeccompProfile(). + WithType(corev1.SeccompProfileTypeRuntimeDefault)) + if !utils.IsOpenShift() { + securityContext = securityContext. + WithRunAsUser(mocoMySQLRunAsUser). + WithRunAsGroup(mocoMySQLRunAsGroup) + } + return securityContext +} + +func mocoOverwriteContainers() []mocov1beta2.OverwriteContainer { + securityContext := (*mocov1beta2.SecurityContextApplyConfiguration)(mocoContainerSecurityContext()) + return []mocov1beta2.OverwriteContainer{ + { + Name: mocov1beta2.AgentContainerName, + SecurityContext: securityContext.DeepCopy(), + }, + { + Name: mocov1beta2.InitContainerName, + SecurityContext: securityContext.DeepCopy(), + }, + { + Name: mocov1beta2.SlowQueryLogAgentContainerName, + SecurityContext: securityContext.DeepCopy(), + }, + { + Name: mocov1beta2.ExporterContainerName, + SecurityContext: securityContext.DeepCopy(), + }, + } +} + +func buildPVCSpec(storageSize string) mocov1beta2.PersistentVolumeClaimSpecApplyConfiguration { + quantity, _ := resource.ParseQuantity(storageSize) + pvcSpec := corev1ac.PersistentVolumeClaimSpec(). + WithAccessModes(corev1.ReadWriteOnce). + WithResources( + corev1ac.VolumeResourceRequirements(). + WithRequests(corev1.ResourceList{ + corev1.ResourceStorage: quantity, + }), + ) + return mocov1beta2.PersistentVolumeClaimSpecApplyConfiguration(*pvcSpec) +} + +func BuildWandbMysqlLabels(wandb *apiv2.WeightsAndBiases) map[string]string { + return common.BuildWandbLabels(wandb, MysqlModuleName) +} + +func ToMysqlOnDeleteRule(wandb *apiv2.WeightsAndBiases, retentionPolicy apiv2.RetentionPolicy) common.OnDeleteRule { + return common.ToOnDeleteRule(wandb, retentionPolicy, MysqlModuleName) +} diff --git a/internal/controller/infra/managed/mysql/moco/spec_test.go b/internal/controller/infra/managed/mysql/moco/spec_test.go new file mode 100644 index 00000000..fa1ac7a2 --- /dev/null +++ b/internal/controller/infra/managed/mysql/moco/spec_test.go @@ -0,0 +1,291 @@ +package moco + +import ( + "context" + + mocov1beta2 "github.com/cybozu-go/moco/api/v1beta2" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/samber/lo" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/pkg/wandb/manifest" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/pkg/utils" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + corev1ac "k8s.io/client-go/applyconfigurations/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var _ = Describe("Moco MySQL specs", func() { + BeforeEach(func() { + utils.SetOpenShiftMode(false) + }) + + It("renders hardened pod and container security settings", func() { + cluster, _, err := ToMocoMySQLClusterSpec( + context.Background(), + apiv2.ManagedMysqlSpec{ + Name: "mysql", + Namespace: "wandb", + Replicas: 3, + StorageSize: "10Gi", + }, + mocoWandb(), + mocoScheme(), + manifest.Manifest{}, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(cluster).NotTo(BeNil()) + + podSpec := cluster.Spec.PodTemplate.Spec + expectMocoPodSecurityContext(podSpec.SecurityContext) + Expect(podSpec.Containers).To(HaveLen(1)) + Expect(*podSpec.Containers[0].Name).To(Equal("mysqld")) + expectMocoContainerSecurityContext(podSpec.Containers[0].SecurityContext) + + Expect(cluster.Spec.PodTemplate.OverwriteContainers).To(HaveLen(4)) + for _, overwrite := range cluster.Spec.PodTemplate.OverwriteContainers { + Expect(overwrite.Name).To(BeElementOf( + mocov1beta2.AgentContainerName, + mocov1beta2.InitContainerName, + mocov1beta2.SlowQueryLogAgentContainerName, + mocov1beta2.ExporterContainerName, + )) + Expect(overwrite.SecurityContext).NotTo(BeNil()) + expectMocoContainerSecurityContext((*corev1ac.SecurityContextApplyConfiguration)(overwrite.SecurityContext)) + } + }) + + It("omits fixed Moco IDs in OpenShift mode", func() { + utils.SetOpenShiftMode(true) + + cluster, _, err := ToMocoMySQLClusterSpec( + context.Background(), + apiv2.ManagedMysqlSpec{ + Name: "mysql", + Namespace: "wandb", + Replicas: 3, + StorageSize: "10Gi", + }, + mocoWandb(), + mocoScheme(), + manifest.Manifest{}, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(cluster).NotTo(BeNil()) + + podSpec := cluster.Spec.PodTemplate.Spec + expectMocoOpenShiftPodSecurityContext(podSpec.SecurityContext) + expectMocoOpenShiftContainerSecurityContext(podSpec.Containers[0].SecurityContext) + }) + + It("sets mysqld_exporter collectors only when telemetry is enabled", func() { + // A non-empty Collectors list is what makes Moco inject the mysqld_exporter sidecar. + enabled, _, err := ToMocoMySQLClusterSpec( + context.Background(), + apiv2.ManagedMysqlSpec{ + Name: "mysql", Namespace: "wandb", Replicas: 3, StorageSize: "10Gi", + Telemetry: apiv2.Telemetry{Enabled: true}, + }, + mocoWandb(), + mocoScheme(), + manifest.Manifest{}, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(enabled.Spec.Collectors).NotTo(BeEmpty()) + + disabled, _, err := ToMocoMySQLClusterSpec( + context.Background(), + apiv2.ManagedMysqlSpec{ + Name: "mysql", Namespace: "wandb", Replicas: 3, StorageSize: "10Gi", + Telemetry: apiv2.Telemetry{Enabled: false}, + }, + mocoWandb(), + mocoScheme(), + manifest.Manifest{}, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(disabled.Spec.Collectors).To(BeEmpty()) + }) + + DescribeTable("refuses to forward a replica count Moco rejects", + func(replicas int32) { + ctx := context.Background() + nn := types.NamespacedName{Name: "mysql", Namespace: "wandb"} + cl := fake.NewClientBuilder().WithScheme(mocoScheme()).Build() + + desired, cm, err := ToMocoMySQLClusterSpec( + ctx, + apiv2.ManagedMysqlSpec{Name: "mysql", Namespace: "wandb", Replicas: replicas, StorageSize: "10Gi"}, + mocoWandb(), + mocoScheme(), + manifest.Manifest{}, + ) + Expect(err).NotTo(HaveOccurred()) + + conditions := WriteState(ctx, cl, nn, desired, cm, nil) + + reconciled, found := lo.Find(conditions, func(c metav1.Condition) bool { + return c.Type == common.ReconciledType + }) + Expect(found).To(BeTrue()) + Expect(reconciled.Status).To(Equal(metav1.ConditionFalse)) + Expect(reconciled.Reason).To(Equal(InvalidReplicaCountReason)) + + // an invalid count must not create a cluster + got := &mocov1beta2.MySQLCluster{} + Expect(apierrors.IsNotFound(cl.Get(ctx, nn, got))).To(BeTrue()) + }, + Entry("even count", int32(2)), + Entry("zero / unset", int32(0)), + ) + + It("stamps wandb labels onto Moco's data PVCs", func() { + ctx := context.Background() + wandbLabels := map[string]string{"app.kubernetes.io/managed-by": "wandb"} + + // Moco names PVCs "<dataVolumeName>-<PrefixedName()>-<ordinal>". + mocoPVC := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{Name: "mysql-data-moco-mysql-0", Namespace: "wandb"}, + } + // A PVC matching the old (pre-Moco) "datadir-" name must NOT be matched. + legacyPVC := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{Name: "datadir-mysql-0", Namespace: "wandb"}, + } + cl := fake.NewClientBuilder(). + WithScheme(mocoScheme()). + WithObjects(mocoPVC, legacyPVC). + Build() + + Expect(ensurePVCLabels(ctx, cl, "wandb", "mysql", wandbLabels)).To(Succeed()) + + got := &corev1.PersistentVolumeClaim{} + Expect(cl.Get(ctx, types.NamespacedName{Name: "mysql-data-moco-mysql-0", Namespace: "wandb"}, got)).To(Succeed()) + Expect(got.Labels).To(HaveKeyWithValue("app.kubernetes.io/managed-by", "wandb")) + + legacy := &corev1.PersistentVolumeClaim{} + Expect(cl.Get(ctx, types.NamespacedName{Name: "datadir-mysql-0", Namespace: "wandb"}, legacy)).To(Succeed()) + Expect(legacy.Labels).NotTo(HaveKey("app.kubernetes.io/managed-by")) + }) + + It("backstops a scale-down the webhook can't see, leaving the cluster untouched", func() { + ctx := context.Background() + nn := types.NamespacedName{Name: "mysql", Namespace: "wandb"} + + existing := &mocov1beta2.MySQLCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "mysql", Namespace: "wandb"}, + Spec: mocov1beta2.MySQLClusterSpec{Replicas: 3}, + } + cl := fake.NewClientBuilder().WithScheme(mocoScheme()).WithObjects(existing).Build() + + desired, cm, err := ToMocoMySQLClusterSpec( + ctx, + apiv2.ManagedMysqlSpec{Name: "mysql", Namespace: "wandb", Replicas: 1, StorageSize: "10Gi"}, + mocoWandb(), + mocoScheme(), + manifest.Manifest{}, + ) + Expect(err).NotTo(HaveOccurred()) + + conditions := WriteState(ctx, cl, nn, desired, cm, nil) + + reconciled, found := lo.Find(conditions, func(c metav1.Condition) bool { + return c.Type == common.ReconciledType + }) + Expect(found).To(BeTrue()) + Expect(reconciled.Status).To(Equal(metav1.ConditionFalse)) + Expect(reconciled.Reason).To(Equal(ScaleDownUnsupportedReason)) + Expect(reconciled.Message).To(ContainSubstring("from 3 to 1 replicas")) + + got := &mocov1beta2.MySQLCluster{} + Expect(cl.Get(ctx, nn, got)).To(Succeed()) + Expect(got.Spec.Replicas).To(Equal(int32(3))) + }) +}) + +func mocoScheme() *runtime.Scheme { + scheme := runtime.NewScheme() + Expect(apiv2.AddToScheme(scheme)).To(Succeed()) + Expect(mocov1beta2.AddToScheme(scheme)).To(Succeed()) + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + return scheme +} + +func mocoWandb() *apiv2.WeightsAndBiases { + return &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiv2.GroupVersion.String(), + Kind: "WeightsAndBiases", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "wandb", + }, + } +} + +func expectMocoPodSecurityContext(securityContext *corev1ac.PodSecurityContextApplyConfiguration) { + Expect(securityContext).NotTo(BeNil()) + Expect(securityContext.RunAsUser).NotTo(BeNil()) + Expect(*securityContext.RunAsUser).To(Equal(mocoMySQLRunAsUser)) + Expect(securityContext.RunAsGroup).NotTo(BeNil()) + Expect(*securityContext.RunAsGroup).To(Equal(mocoMySQLRunAsGroup)) + Expect(securityContext.RunAsNonRoot).NotTo(BeNil()) + Expect(*securityContext.RunAsNonRoot).To(BeTrue()) + Expect(securityContext.FSGroup).NotTo(BeNil()) + Expect(*securityContext.FSGroup).To(Equal(mocoMySQLFSGroup)) + Expect(securityContext.FSGroupChangePolicy).NotTo(BeNil()) + Expect(*securityContext.FSGroupChangePolicy).To(Equal(corev1.FSGroupChangeOnRootMismatch)) + Expect(securityContext.SeccompProfile).NotTo(BeNil()) + Expect(securityContext.SeccompProfile.Type).NotTo(BeNil()) + Expect(*securityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) +} + +func expectMocoContainerSecurityContext(securityContext *corev1ac.SecurityContextApplyConfiguration) { + Expect(securityContext).NotTo(BeNil()) + Expect(securityContext.RunAsUser).NotTo(BeNil()) + Expect(*securityContext.RunAsUser).To(Equal(mocoMySQLRunAsUser)) + Expect(securityContext.RunAsGroup).NotTo(BeNil()) + Expect(*securityContext.RunAsGroup).To(Equal(mocoMySQLRunAsGroup)) + Expect(securityContext.RunAsNonRoot).NotTo(BeNil()) + Expect(*securityContext.RunAsNonRoot).To(BeTrue()) + Expect(securityContext.AllowPrivilegeEscalation).NotTo(BeNil()) + Expect(*securityContext.AllowPrivilegeEscalation).To(BeFalse()) + Expect(securityContext.Capabilities).NotTo(BeNil()) + Expect(securityContext.Capabilities.Drop).To(ContainElement(mocoMySQLCapabilityAll)) + Expect(securityContext.SeccompProfile).NotTo(BeNil()) + Expect(securityContext.SeccompProfile.Type).NotTo(BeNil()) + Expect(*securityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) +} + +func expectMocoOpenShiftPodSecurityContext(securityContext *corev1ac.PodSecurityContextApplyConfiguration) { + Expect(securityContext).NotTo(BeNil()) + Expect(securityContext.RunAsUser).To(BeNil()) + Expect(securityContext.RunAsGroup).To(BeNil()) + Expect(securityContext.FSGroup).To(BeNil()) + Expect(securityContext.FSGroupChangePolicy).To(BeNil()) + Expect(securityContext.RunAsNonRoot).NotTo(BeNil()) + Expect(*securityContext.RunAsNonRoot).To(BeTrue()) + Expect(securityContext.SeccompProfile).NotTo(BeNil()) + Expect(securityContext.SeccompProfile.Type).NotTo(BeNil()) + Expect(*securityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) +} + +func expectMocoOpenShiftContainerSecurityContext(securityContext *corev1ac.SecurityContextApplyConfiguration) { + Expect(securityContext).NotTo(BeNil()) + Expect(securityContext.RunAsUser).To(BeNil()) + Expect(securityContext.RunAsGroup).To(BeNil()) + Expect(securityContext.RunAsNonRoot).NotTo(BeNil()) + Expect(*securityContext.RunAsNonRoot).To(BeTrue()) + Expect(securityContext.AllowPrivilegeEscalation).NotTo(BeNil()) + Expect(*securityContext.AllowPrivilegeEscalation).To(BeFalse()) + Expect(securityContext.Capabilities).NotTo(BeNil()) + Expect(securityContext.Capabilities.Drop).To(ContainElement(mocoMySQLCapabilityAll)) + Expect(securityContext.SeccompProfile).NotTo(BeNil()) + Expect(securityContext.SeccompProfile.Type).NotTo(BeNil()) + Expect(*securityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) +} diff --git a/internal/controller/infra/managed/mysql/moco/status.go b/internal/controller/infra/managed/mysql/moco/status.go new file mode 100644 index 00000000..fad30b08 --- /dev/null +++ b/internal/controller/infra/managed/mysql/moco/status.go @@ -0,0 +1,215 @@ +package moco + +import ( + "context" + "time" + + "github.com/samber/lo" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" +) + +const ( + MySQLCustomResourceType = "MySQLCustomResource" + MySQLConnectionInfoType = "MySQLConnectionInfo" + MySQLReportedReadyType = "MySQLReportedReady" +) + +func ComputeStatus( + ctx context.Context, + enabled bool, + oldConditions, currentConditions []metav1.Condition, + connection *apiv2.MysqlConnection, + currentGeneration int64, +) (apiv2.MysqlInfraStatus, []corev1.Event, ctrl.Result) { + ctx, _ = logx.WithSlog(ctx, logx.Mysql) + result := apiv2.MysqlInfraStatus{} + + if connection != nil { + result.Connection = *connection + } + + currentConditions = applyDefaultConditions(currentConditions) + + result.Conditions = common.ComputeConditionUpdates( + oldConditions, + currentConditions, + currentGeneration, + common.DefaultConditionExpiry, + ) + + state, events := inferInfraState(ctx, enabled, result.Conditions) + result.State = state + + result.Ready = !lo.Contains(common.NotReadyStates, result.State) + + requeueAfter := 3 * time.Minute + switch result.State { + case common.ErrorState: + requeueAfter = 15 * time.Second + case common.DegradedState: + requeueAfter = 5 * time.Minute + case common.PendingState: + requeueAfter = 2 * time.Minute + case common.HealthyState: + requeueAfter = 10 * time.Minute + } + + return result, events, ctrl.Result{RequeueAfter: requeueAfter} +} + +func applyDefaultConditions(conditions []metav1.Condition) []metav1.Condition { + if !common.ContainsType(conditions, MySQLConnectionInfoType) { + conditions = append(conditions, metav1.Condition{ + Type: MySQLConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }) + } + + return conditions +} + +func inferInfraState( + ctx context.Context, + enabled bool, + conditions []metav1.Condition, +) (string, []corev1.Event) { + if !enabled { + return common.UnavailableState, nil + } + var events []corev1.Event + impliedStates := make(map[string]string, len(conditions)) + + impliedStates = inferStateFromCondition(ctx, MySQLCustomResourceType, impliedStates, conditions) + impliedStates = inferStateFromCondition(ctx, MySQLConnectionInfoType, impliedStates, conditions) + impliedStates = inferStateFromCondition(ctx, MySQLReportedReadyType, impliedStates, conditions) + + hasImpliedState := func(target string) bool { + return len(lo.FilterValues( + impliedStates, + func(_ string, value string) bool { + return value == target + })) > 0 + } + + summaryState := common.UnknownState + + // if the service is reporting as healthy but the connection info is unavailable + // log the missing connection as an event and mark the infraStatus as 'error' + if impliedStates[MySQLConnectionInfoType] == common.UnavailableState && + impliedStates[MySQLReportedReadyType] == common.HealthyState { + events = append(events, corev1.Event{ + Type: corev1.EventTypeWarning, + Reason: "MySQLConnectionInfoUnavailable", + Message: "MySQL connection info is unavailable, but MySQL is reported as ready.", + }) + summaryState = common.ErrorState + } + + // if there is a specific state identified, use that + if summaryState != common.UnknownState { + return summaryState, events + } + + // otherwise, return the most significant state mapped for any condition + stateSignificanceOrder := []string{ + common.ErrorState, + common.UnavailableState, + common.PendingState, + common.DegradedState, + common.HealthyState, + } + for _, s := range stateSignificanceOrder { + if hasImpliedState(s) { + return s, events + } + } + + return common.UnknownState, events +} + +func inferStateFromCondition(ctx context.Context, conditionType string, impliedStates map[string]string, conditions []metav1.Condition) map[string]string { + cond, found := lo.Find(conditions, func(c metav1.Condition) bool { return c.Type == conditionType }) + if !found { + impliedStates[conditionType] = common.UnknownState + } else { + switch conditionType { + case MySQLCustomResourceType: + impliedStates[conditionType] = inferState_MySQLCustomResourceType(ctx, cond) + case MySQLConnectionInfoType: + impliedStates[conditionType] = inferState_MySQLConnectionInfoType(ctx, cond) + case MySQLReportedReadyType: + impliedStates[conditionType] = inferState_MySQLReportedReadyType(ctx, cond) + default: + impliedStates[conditionType] = common.UnknownState + } + } + return impliedStates +} + +func inferState_MySQLCustomResourceType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + if condition.Reason == common.PendingCreateReason { + result = common.PendingState + } + if condition.Reason == common.PendingDeleteReason { + result = common.UnavailableState + } + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} + +func inferState_MySQLConnectionInfoType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + result = common.UnavailableState + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} + +func inferState_MySQLReportedReadyType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + switch condition.Reason { + case "Pending", "Progressing", "Initializing": + result = common.PendingState + case "Error", "Failed": + result = common.ErrorState + case "Terminating": + result = common.UnavailableState + default: + result = common.UnavailableState + } + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} diff --git a/internal/controller/infra/managed/mysql/moco/write.go b/internal/controller/infra/managed/mysql/moco/write.go new file mode 100644 index 00000000..15325fb2 --- /dev/null +++ b/internal/controller/infra/managed/mysql/moco/write.go @@ -0,0 +1,225 @@ +package moco + +import ( + "context" + "fmt" + "maps" + "strings" + + mocov1beta2 "github.com/cybozu-go/moco/api/v1beta2" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + ResourceTypeName = "InnoDBCluster" + AppConnTypeName = "MySQLAppConn" + + // InvalidReplicaCountReason: manifest sizing yielded a replica count Moco rejects. + InvalidReplicaCountReason = "InvalidReplicaCount" + + // ScaleDownUnsupportedReason: a reconcile would shrink the running cluster, which Moco forbids. + ScaleDownUnsupportedReason = "ScaleDownUnsupported" +) + +func WriteState( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + desired *mocov1beta2.MySQLCluster, + confMap *corev1.ConfigMap, + wandbLabels map[string]string, +) []metav1.Condition { + ctx, _ = logx.WithSlog(ctx, logx.Mysql) + var actual = &mocov1beta2.MySQLCluster{} + + nsnBuilder := createNsNameBuilder(specNamespacedName) + + found, err := common.GetResource( + ctx, cl, nsnBuilder.ClusterNsName(), ResourceTypeName, actual, + ) + if err != nil { + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }, + { + Type: MySQLCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: common.ApiErrorReason, + }, + } + } + if !found { + actual = nil + } + + // MOCO's mutating admission webhook fills in spec.serverIDBase with a random + // positive integer on Create; its validating webhook requires the value to + // stay positive on Update. Our generated desired spec leaves the field at + // zero, which would re-trip validation. Preserve the live value. + if actual != nil { + desired.Spec.ServerIDBase = actual.Spec.ServerIDBase + } + + // Sizing is resolved from the manifest at reconcile time, after the CR + // admission webhook runs, so a bad value reaches here unvalidated. + if !apiv2.ValidMysqlReplicaCount(desired.Spec.Replicas) { + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: InvalidReplicaCountReason, + Message: fmt.Sprintf( + "manifest sizing produced %d MySQL replicas; Moco requires a positive odd number", + desired.Spec.Replicas, + ), + }, + } + } + + // Backstop for the webhook's scale-down check: the webhook only sees the CR + // (explicit edits), not the manifest-resolved count or the live cluster. + // Catch those shrinks here instead of letting Moco reject them opaquely. + if actual != nil && desired.Spec.Replicas < actual.Spec.Replicas { + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: ScaleDownUnsupportedReason, + Message: fmt.Sprintf( + "cannot scale managed MySQL down from %d to %d replicas; Moco does not support in-place replica reduction (use its manual stop-clustering procedure)", + actual.Spec.Replicas, desired.Spec.Replicas, + ), + }, + { + Type: MySQLCustomResourceType, + Status: metav1.ConditionTrue, + Reason: common.ResourceExistsReason, + }, + } + } + + result := make([]metav1.Condition, 0) + + if confMap != nil { + var actualConfMap = &corev1.ConfigMap{} + cmNsName := types.NamespacedName{Name: confMap.Name, Namespace: confMap.Namespace} + cmFound, cmErr := common.GetResource(ctx, cl, cmNsName, "ConfigMap", actualConfMap) + if cmErr != nil { + result = append(result, metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }) + } else { + if !cmFound { + actualConfMap = nil + } + if _, cmErr := common.CrudResource(ctx, cl, confMap, actualConfMap); cmErr != nil { + result = append(result, metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }) + } + } + } + + action, err := common.CrudResource(ctx, cl, desired, actual) + if err != nil { + result = append(result, metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }) + } + + switch action { + case common.CreateAction: + result = append(result, metav1.Condition{ + Type: MySQLCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.PendingCreateReason, + }) + case common.DeleteAction: + result = append(result, metav1.Condition{ + Type: MySQLCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.PendingDeleteReason, + }) + case common.UpdateAction, common.UnchangedAction: + result = append(result, metav1.Condition{ + Type: MySQLCustomResourceType, + Status: metav1.ConditionTrue, + Reason: common.ResourceExistsReason, + }) + case common.NoAction: + result = append(result, metav1.Condition{ + Type: MySQLCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }) + } + + if len(wandbLabels) > 0 { + if err := ensurePVCLabels(ctx, cl, specNamespacedName.Namespace, nsnBuilder.ClusterName(), wandbLabels); err != nil { + result = append(result, metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }) + } + } + + return result +} + +// ensurePVCLabels stamps the wandb labels onto Moco's PVCs (missing because Moco +// doesn't propagate them through its StatefulSet volumeClaimTemplates), so +// purgeAssociatedResources can select them by label on teardown. Moco names PVCs +// "<dataVolumeName>-<cluster.PrefixedName()>-<ordinal>" (see Moco pvc.go); the +// prefix is built from those same sources so it can't drift from upstream. +func ensurePVCLabels( + ctx context.Context, + cl client.Client, + namespace string, + clusterName string, + labels map[string]string, +) error { + log := logx.GetSlog(ctx) + cluster := &mocov1beta2.MySQLCluster{ObjectMeta: metav1.ObjectMeta{Name: clusterName}} + prefix := fmt.Sprintf("%s-%s-", dataVolumeName, cluster.PrefixedName()) + + pvcList := &corev1.PersistentVolumeClaimList{} + if err := cl.List(ctx, pvcList, &client.ListOptions{Namespace: namespace}); err != nil { + return err + } + + for _, pvc := range pvcList.Items { + if !strings.HasPrefix(pvc.Name, prefix) { + continue + } + if common.HasAllLabelKeys(pvc.Labels, labels) { + continue + } + patch := client.MergeFrom(pvc.DeepCopy()) + if pvc.Labels == nil { + pvc.Labels = make(map[string]string) + } + maps.Copy(pvc.Labels, labels) + if err := cl.Patch(ctx, &pvc, patch); err != nil { + log.Error("failed to patch PVC labels", logx.ErrAttr(err), "pvc", pvc.Name) + return err + } + log.Debug("patched wandb labels onto PVC", "pvc", pvc.Name) + } + return nil +} diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/config.go b/internal/controller/infra/managed/objectstore/seaweedfs/config.go new file mode 100644 index 00000000..f2ec8276 --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/config.go @@ -0,0 +1,69 @@ +package seaweedfs + +import ( + "encoding/json" + "fmt" +) + +type SeaweedS3Config struct { + AccessKey string +} + +type s3Identity struct { + Name string `json:"name"` + Credentials []s3Credential `json:"credentials"` + Actions []string `json:"actions"` +} + +type s3Credential struct { + AccessKey string `json:"accessKey"` + SecretKey string `json:"secretKey"` +} + +type s3IdentityConfig struct { + Identities []s3Identity `json:"identities"` +} + +func buildS3IdentityConfig(accessKey, secretKey string) s3IdentityConfig { + return s3IdentityConfig{ + Identities: []s3Identity{ + { + Name: accessKey, + Credentials: []s3Credential{ + { + AccessKey: accessKey, + SecretKey: secretKey, + }, + }, + Actions: []string{"Admin", "Read", "Write", "List", "Tagging", "Lock"}, + }, + }, + } +} + +func (c s3IdentityConfig) toJSON() (string, error) { + data, err := json.MarshalIndent(c, "", " ") + if err != nil { + return "", fmt.Errorf("failed to marshal S3 identity config: %w", err) + } + return string(data), nil +} + +func parseS3IdentityConfig(data string) (s3IdentityConfig, error) { + var config s3IdentityConfig + if err := json.Unmarshal([]byte(data), &config); err != nil { + return s3IdentityConfig{}, err + } + return config, nil +} + +func extractSecretKey(config s3IdentityConfig, accessKey string) (string, error) { + for _, identity := range config.Identities { + for _, credential := range identity.Credentials { + if credential.AccessKey == accessKey && credential.SecretKey != "" { + return credential.SecretKey, nil + } + } + } + return "", fmt.Errorf("secret key not found for access key %s", accessKey) +} diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/config_test.go b/internal/controller/infra/managed/objectstore/seaweedfs/config_test.go new file mode 100644 index 00000000..b762e618 --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/config_test.go @@ -0,0 +1,64 @@ +package seaweedfs + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("S3IdentityConfig", func() { + Describe("Round-trip parsing", func() { + DescribeTable("should correctly parse config generated by toJSON", + func(accessKey, secretKey string) { + original := buildS3IdentityConfig(accessKey, secretKey) + jsonStr, err := original.toJSON() + Expect(err).ToNot(HaveOccurred()) + + parsed, err := parseS3IdentityConfig(jsonStr) + Expect(err).ToNot(HaveOccurred()) + + Expect(parsed.Identities).To(HaveLen(1)) + Expect(parsed.Identities[0].Credentials).To(HaveLen(1)) + Expect(parsed.Identities[0].Credentials[0].AccessKey).To(Equal(accessKey)) + Expect(parsed.Identities[0].Credentials[0].SecretKey).To(Equal(secretKey)) + }, + Entry("basic credentials", "admin", "password123"), + Entry("with special characters", "user", "p@ssw0rd!#$"), + Entry("long credentials", "very-long-username", "very-long-password-with-many-characters"), + Entry("minimum values", "a", "b"), + ) + + It("should extract secret key from config", func() { + config := buildS3IdentityConfig("admin", "mysecret") + key, err := extractSecretKey(config, "admin") + Expect(err).ToNot(HaveOccurred()) + Expect(key).To(Equal("mysecret")) + }) + + It("should return error for empty config", func() { + config := s3IdentityConfig{} + _, err := extractSecretKey(config, "admin") + Expect(err).To(HaveOccurred()) + }) + + It("should handle malformed JSON", func() { + _, err := parseS3IdentityConfig("not json") + Expect(err).To(HaveOccurred()) + }) + + Context("idempotency", func() { + It("should be idempotent - parse(toJSON(parse(toJSON(x)))) == parse(toJSON(x))", func() { + original := buildS3IdentityConfig("admin", "password123") + + firstJSON, err := original.toJSON() + Expect(err).ToNot(HaveOccurred()) + firstParsed, err := parseS3IdentityConfig(firstJSON) + Expect(err).ToNot(HaveOccurred()) + + secondJSON, err := firstParsed.toJSON() + Expect(err).ToNot(HaveOccurred()) + + Expect(secondJSON).To(Equal(firstJSON)) + }) + }) + }) +}) diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/conn.go b/internal/controller/infra/managed/objectstore/seaweedfs/conn.go new file mode 100644 index 00000000..af96891d --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/conn.go @@ -0,0 +1,156 @@ +package seaweedfs + +import ( + "context" + "fmt" + "net/url" + "strconv" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + S3UrlScheme = "s3" + S3Port = "8333" +) + +type s3ConnInfo struct { + AccessKey string + SecretKey string + Host string + Port string + Bucket string + TLS bool + ForcePathStyle bool +} + +func buildS3ConnInfo( + accessKey, secretKey string, nsnBuilder *NsNameBuilder, tls bool, +) *s3ConnInfo { + return &s3ConnInfo{ + AccessKey: accessKey, + TLS: tls, + SecretKey: secretKey, + Host: s3ServiceHost(nsnBuilder.SpecName(), nsnBuilder.Namespace()), + Port: S3Port, + Bucket: "bucket", + ForcePathStyle: true, + } +} + +func s3ServiceHost(specName, namespace string) string { + return fmt.Sprintf("%s-s3.%s.svc.cluster.local", SeaweedName(specName), namespace) +} + +// s3ExternalURL is the endpoint the W&B server signs S3 requests against +// (it presigns with this host and rewrites the URL for external clients +// without re-signing). The s3 gateway must verify signatures against this +// host rather than the Host/X-Forwarded-Host of proxied requests. +func s3ExternalURL(specName, namespace string, tls bool) string { + return fmt.Sprintf("%s://%s:%s", s3Scheme(tls), s3ServiceHost(specName, namespace), S3Port) +} + +func s3Scheme(tls bool) string { + if tls { + return "https" + } + return "http" +} + +func (s *s3ConnInfo) toUrl() *url.URL { + return &url.URL{ + Scheme: S3UrlScheme, + Host: fmt.Sprintf("%s:%s", s.Host, s.Port), + User: url.UserPassword(s.AccessKey, s.SecretKey), + Path: s.Bucket, + } +} + +func (s *s3ConnInfo) scheme() string { + return s3Scheme(s.TLS) +} + +func writeWandbConnInfo( + ctx context.Context, + cl client.Client, + owner client.Object, + nsnBuilder *NsNameBuilder, + connInfo *s3ConnInfo, +) ( + *apiv2.ObjectStoreConnection, error, +) { + var err error + var found bool + var gvk schema.GroupVersionKind + var actual = &corev1.Secret{} + + nsName := nsnBuilder.ConnectionNsName() + urlKey := "url" + + if found, err = common.GetResource( + ctx, cl, nsName, AppConnTypeName, actual, + ); err != nil { + return nil, err + } + if !found { + actual = nil + } + + if gvk, err = cl.GroupVersionKindFor(owner); err != nil { + return nil, fmt.Errorf("could not get GVK for owner: %w", err) + } + ref := metav1.OwnerReference{ + APIVersion: gvk.GroupVersion().String(), + Kind: gvk.Kind, + Name: owner.GetName(), + UID: owner.GetUID(), + Controller: ptr.To(false), + BlockOwnerDeletion: ptr.To(false), + } + + desired := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsName.Name, + Namespace: nsName.Namespace, + OwnerReferences: []metav1.OwnerReference{ref}, + }, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{ + urlKey: fmt.Sprintf("%s?tls=%t", connInfo.toUrl().String(), connInfo.TLS), + "Host": connInfo.Host, + "Port": connInfo.Port, + "AccessKey": connInfo.AccessKey, + "SecretKey": connInfo.SecretKey, + "Region": "us-east-1", + "Bucket": connInfo.Bucket, + "Scheme": connInfo.scheme(), + "TlsEnabled": strconv.FormatBool(connInfo.TLS), + "Provider": "s3", + "ForcePathStyle": strconv.FormatBool(connInfo.ForcePathStyle), + }, + } + + if _, err = common.CrudResource(ctx, cl, desired, actual); err != nil { + return nil, err + } + + localRef := corev1.LocalObjectReference{Name: nsName.Name} + return &apiv2.ObjectStoreConnection{ + URL: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: urlKey, Optional: ptr.To(false)}, + Endpoint: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Host", Optional: ptr.To(false)}, + Port: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Port", Optional: ptr.To(false)}, + AccessKey: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "AccessKey", Optional: ptr.To(false)}, + SecretKey: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "SecretKey", Optional: ptr.To(false)}, + Region: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Region", Optional: ptr.To(false)}, + Bucket: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Bucket", Optional: ptr.To(false)}, + TlsEnabled: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "TlsEnabled", Optional: ptr.To(false)}, + Provider: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Provider", Optional: ptr.To(false)}, + ForcePathStyle: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "ForcePathStyle", Optional: ptr.To(false)}, + }, nil +} diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/detach.go b/internal/controller/infra/managed/objectstore/seaweedfs/detach.go new file mode 100644 index 00000000..3cedc43e --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/detach.go @@ -0,0 +1,94 @@ +package seaweedfs + +import ( + "context" + "fmt" + + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + seaweedv1 "github.com/wandb/operator/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func CheckDetached( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + wandbUID types.UID, + desiredReplicas int32, +) []metav1.Condition { + nsnBuilder := createNsNameBuilder(specNamespacedName) + actual := &seaweedv1.Seaweed{} + found, err := common.GetResource(ctx, cl, nsnBuilder.SpecNsName(), ResourceTypeName, actual) + if err != nil || !found { + return nil + } + if !common.IsDetached(actual, wandbUID) { + return nil + } + + if desiredReplicas > 0 && actual.Spec.Volume != nil && actual.Spec.Volume.Replicas != desiredReplicas { + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.DetachedSpecMismatch, + Message: fmt.Sprintf("detached Seaweed CR spec mismatch: volume replicas want %d, have %d", desiredReplicas, actual.Spec.Volume.Replicas), + }, + } + + } + return nil +} + +func DetachFinalizer( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + wandbOwner client.Object, +) error { + ctx, log := logx.WithSlog(ctx, logx.ObjectStore) + nsnBuilder := createNsNameBuilder(specNamespacedName) + + var actual = &seaweedv1.Seaweed{} + found, err := common.GetResource(ctx, cl, nsnBuilder.SpecNsName(), ResourceTypeName, actual) + if err != nil { + return err + } + if !found { + log.Info("abort detach finalizer: Seaweed CR not found") + return nil + } + + if common.IsDetached(actual, wandbOwner.GetUID()) { + log.Debug("Seaweed CR already detached") + return nil + } + + common.RemoveOwnerReference(actual, wandbOwner.GetUID()) + if err = cl.Update(ctx, actual); err != nil { + if errors.IsNotFound(err) { + return nil + } + log.Error("error detaching Seaweed CR", logx.ErrAttr(err)) + return err + } + log.Info("detached Seaweed CR", "name", actual.Name) + + secret := &corev1.Secret{} + found, err = common.GetResource(ctx, cl, nsnBuilder.ConnectionNsName(), "Secret", secret) + if err != nil || !found { + return err + } + common.RemoveOwnerReference(secret, wandbOwner.GetUID()) + if err = cl.Update(ctx, secret); err != nil && !errors.IsNotFound(err) { + log.Error("error detaching connection secret", logx.ErrAttr(err)) + return err + } + log.Info("detached connection secret", "name", nsnBuilder.ConnectionNsName().Name) + return nil +} diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/image_test.go b/internal/controller/infra/managed/objectstore/seaweedfs/image_test.go new file mode 100644 index 00000000..71af84fd --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/image_test.go @@ -0,0 +1,31 @@ +package seaweedfs + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/wandb/operator/pkg/wandb/manifest" +) + +var _ = Describe("SeaweedImage", func() { + DescribeTable("resolves the image string", + func(img manifest.ImageRef, globalImageRegistry, expected string) { + Expect(SeaweedImage(img, globalImageRegistry)).To(Equal(expected)) + }, + + // 1) No image from the manifest: fall back to the hardcoded default. + Entry("no manifest image", manifest.ImageRef{}, "", defaultSeaweedImage), + + // 2) Manifest supplies the image, no global registry: use it verbatim. + Entry("manifest image", + manifest.ImageRef{Registry: "docker.io", Repository: "chrislusf/seaweedfs", Tag: "4.35"}, + "", + "docker.io/chrislusf/seaweedfs:4.35"), + + // 3) Global image registry is prepended in front of the manifest image. + Entry("manifest image, global registry set", + manifest.ImageRef{Registry: "docker.io", Repository: "chrislusf/seaweedfs", Tag: "4.35"}, + "myregistry.io", + "myregistry.io/docker.io/chrislusf/seaweedfs:4.35"), + ) +}) diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/legacy_minio.go b/internal/controller/infra/managed/objectstore/seaweedfs/legacy_minio.go new file mode 100644 index 00000000..fb1ff7da --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/legacy_minio.go @@ -0,0 +1,173 @@ +package seaweedfs + +import ( + "context" + "fmt" + "strings" + + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var minioTenantGVK = schema.GroupVersionKind{ + Group: "minio.min.io", + Version: "v2", + Kind: "Tenant", +} + +// CleanupLegacyMinio detects and cleans up a MinIO Tenant CR left over from +// before the SeaweedFS migration. It uses an unstructured client so we don't +// need to vendor the MinIO operator types. +// +// If the MinIO CRD is not installed, this is a no-op. +// If a Tenant exists owned by this WeightsAndBiases CR, it is either detached +// (owner reference removed) or purged (deleted along with associated PVCs and +// Secrets) based on the purge parameter. +func CleanupLegacyMinio( + ctx context.Context, + cl client.Client, + wandbName string, + wandbNamespace string, + wandbUID types.UID, + purge bool, + onDeleteSelector labels.Selector, +) error { + ctx, log := logx.WithSlog(ctx, logx.ObjectStore) + + tenantName := fmt.Sprintf("%s-minio", wandbName) + tenantNsName := types.NamespacedName{Name: tenantName, Namespace: wandbNamespace} + + tenant := &unstructured.Unstructured{} + tenant.SetGroupVersionKind(minioTenantGVK) + + err := cl.Get(ctx, tenantNsName, tenant) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + if isNoMatchError(err) { + return nil + } + return err + } + + owned := false + for _, ref := range tenant.GetOwnerReferences() { + if ref.UID == wandbUID { + owned = true + break + } + } + if !owned { + log.Info("legacy MinIO Tenant exists but is not owned by this WeightsAndBiases CR, skipping", + "tenant", tenantName) + return nil + } + + if purge { + log.Info("purging legacy MinIO Tenant CR", "tenant", tenantName) + if err := cl.Delete(ctx, tenant); err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to delete legacy MinIO Tenant: %w", err) + } + if onDeleteSelector != nil { + if err := purgeLegacyMinioResources(ctx, cl, wandbNamespace, onDeleteSelector); err != nil { + return err + } + } + } else { + log.Info("detaching legacy MinIO Tenant CR", "tenant", tenantName) + refs := tenant.GetOwnerReferences() + filtered := make([]interface{}, 0, len(refs)) + for _, ref := range refs { + if ref.UID != wandbUID { + filtered = append(filtered, map[string]interface{}{ + "apiVersion": ref.APIVersion, + "kind": ref.Kind, + "name": ref.Name, + "uid": string(ref.UID), + "controller": ref.Controller, + "blockOwnerDeletion": ref.BlockOwnerDeletion, + }) + } + } + if err := unstructured.SetNestedSlice(tenant.Object, filtered, "metadata", "ownerReferences"); err != nil { + return fmt.Errorf("failed to update owner references: %w", err) + } + if err := cl.Update(ctx, tenant); err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to detach legacy MinIO Tenant: %w", err) + } + } + + connSecretName := fmt.Sprintf("%s-objectstore-connection", wandbName) + connSecret := &corev1.Secret{} + if err := cl.Get(ctx, types.NamespacedName{Name: connSecretName, Namespace: wandbNamespace}, connSecret); err == nil { + if purge { + if err := cl.Delete(ctx, connSecret); err != nil && !errors.IsNotFound(err) { + return err + } + log.Info("deleted legacy MinIO connection secret", "secret", connSecretName) + } else { + common.RemoveOwnerReference(connSecret, wandbUID) + if err := cl.Update(ctx, connSecret); err != nil && !errors.IsNotFound(err) { + return err + } + log.Info("detached legacy MinIO connection secret", "secret", connSecretName) + } + } + + log.Info("legacy MinIO cleanup complete", "tenant", tenantName, "purged", purge) + return nil +} + +func purgeLegacyMinioResources( + ctx context.Context, + cl client.Client, + namespace string, + selector labels.Selector, +) error { + log := logx.GetSlog(ctx) + listOptions := &client.ListOptions{ + Namespace: namespace, + LabelSelector: selector, + } + + pvcList := &corev1.PersistentVolumeClaimList{} + if err := cl.List(ctx, pvcList, listOptions); err != nil { + return err + } + for _, pvc := range pvcList.Items { + if err := cl.Delete(ctx, &pvc); err != nil && !errors.IsNotFound(err) { + return err + } + } + if len(pvcList.Items) > 0 { + log.Info("purged legacy MinIO PVCs", "count", len(pvcList.Items)) + } + + secretList := &corev1.SecretList{} + if err := cl.List(ctx, secretList, listOptions); err != nil { + return err + } + for _, secret := range secretList.Items { + if err := cl.Delete(ctx, &secret); err != nil && !errors.IsNotFound(err) { + return err + } + } + if len(secretList.Items) > 0 { + log.Info("purged legacy MinIO Secrets", "count", len(secretList.Items)) + } + + return nil +} + +func isNoMatchError(err error) bool { + return strings.Contains(err.Error(), "no matches for") || + strings.Contains(err.Error(), "the server could not find the requested resource") +} diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/naming.go b/internal/controller/infra/managed/objectstore/seaweedfs/naming.go new file mode 100644 index 00000000..84bed20e --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/naming.go @@ -0,0 +1,89 @@ +package seaweedfs + +import ( + "fmt" + "strings" + + "github.com/wandb/operator/internal/controller/common" + "k8s.io/apimachinery/pkg/types" +) + +// MaxSpecNameLength is a conservative budget: the seaweedfs operator suffixes +// the Seaweed name ("-master", "-volume", "-filer", peer Services, ordinals); +// 40 leaves 23 chars of DNS-1123 label headroom. +const MaxSpecNameLength = 40 + +const defaultNameSuffix = "-seaweedfs" + +// DefaultSpecName derives the object-store name for a CR instance, shortened +// to the budget; the suffix is preserved so ConnectionName can still strip it. +func DefaultSpecName(crName, instanceKey string) string { + return common.FitDefaultInfraName(common.InstanceBaseName(crName, instanceKey), defaultNameSuffix, MaxSpecNameLength) +} + +type NsNameBuilder struct { + baseNsName types.NamespacedName +} + +func CreateNsNameBuilder(baseNsName types.NamespacedName) *NsNameBuilder { + return &NsNameBuilder{ + baseNsName: baseNsName, + } +} + +func (n *NsNameBuilder) Namespace() string { + return n.baseNsName.Namespace +} + +func (n *NsNameBuilder) SpecName() string { + return n.baseNsName.Name +} + +func (n *NsNameBuilder) SpecNsName() types.NamespacedName { + return types.NamespacedName{ + Namespace: n.Namespace(), + Name: n.SpecName(), + } +} + +func (n *NsNameBuilder) ConfigName() string { + return fmt.Sprintf("%s-s3-config", n.SpecName()) +} + +func (n *NsNameBuilder) ConfigNsName() types.NamespacedName { + return types.NamespacedName{ + Namespace: n.Namespace(), + Name: n.ConfigName(), + } +} + +func (n *NsNameBuilder) ServiceName() string { + return fmt.Sprintf("%s-filer", n.SpecName()) +} + +func (n *NsNameBuilder) connectionBaseName() string { + return strings.TrimSuffix(n.SpecName(), "-seaweedfs") +} + +func (n *NsNameBuilder) ConnectionName() string { + return fmt.Sprintf("%s-objectstore-connection", n.connectionBaseName()) +} + +func (n *NsNameBuilder) ConnectionNsName() types.NamespacedName { + return types.NamespacedName{ + Namespace: n.Namespace(), + Name: n.ConnectionName(), + } +} + +func createNsNameBuilder(baseNsName types.NamespacedName) *NsNameBuilder { + return CreateNsNameBuilder(baseNsName) +} + +func SeaweedName(specName string) string { + return specName +} + +func ConfigName(specName string) string { + return fmt.Sprintf("%s-s3-config", specName) +} diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/purge.go b/internal/controller/infra/managed/objectstore/seaweedfs/purge.go new file mode 100644 index 00000000..58e2c57a --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/purge.go @@ -0,0 +1,83 @@ +package seaweedfs + +import ( + "context" + + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func PurgeFinalizer( + ctx context.Context, + client client.Client, + specNamespacedName types.NamespacedName, + onDeleteRule common.OnDeleteRule, +) error { + ctx, _ = logx.WithSlog(ctx, logx.ObjectStore) + if onDeleteRule.Policy != common.Purge { + return nil + } + return purgeAssociatedResources(ctx, client, specNamespacedName.Namespace, onDeleteRule.Selector) +} + +func purgeAssociatedResources( + ctx context.Context, + cl client.Client, + namespace string, + onDeleteSelector labels.Selector, +) error { + log := logx.GetSlog(ctx) + listOptions := &client.ListOptions{ + Namespace: namespace, + LabelSelector: onDeleteSelector, + } + + pvcList := &corev1.PersistentVolumeClaimList{} + if err := cl.List(ctx, pvcList, listOptions); err != nil { + return err + } + if len(pvcList.Items) > 0 { + log.Info( + "Purging associated PVCs", + "count", len(pvcList.Items), "selector", onDeleteSelector.String(), + ) + } else { + log.Debug( + "No associated PVCs found to purge", + "selector", onDeleteSelector.String(), + ) + } + for _, pvc := range pvcList.Items { + if err := cl.Delete(ctx, &pvc); err != nil && !errors.IsNotFound(err) { + return err + } + } + + secretList := &corev1.SecretList{} + if err := cl.List(ctx, secretList, listOptions); err != nil { + return err + } + if len(secretList.Items) > 0 { + log.Info( + "Purging associated Secrets", + "count", len(secretList.Items), "selector", onDeleteSelector.String(), + ) + } else { + log.Debug( + "No associated Secrets found to purge", + "selector", onDeleteSelector.String(), + ) + } + for _, secret := range secretList.Items { + if err := cl.Delete(ctx, &secret); err != nil && !errors.IsNotFound(err) { + return err + } + } + + return nil +} diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/read.go b/internal/controller/infra/managed/objectstore/seaweedfs/read.go new file mode 100644 index 00000000..0c1803fc --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/read.go @@ -0,0 +1,270 @@ +package seaweedfs + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + ctrlcommon "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + seaweedv1 "github.com/wandb/operator/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const seaweedProbeTimeout = 5 * time.Second + +func ReadState( + ctx context.Context, + k8sClient client.Client, + specNamespacedName types.NamespacedName, + onDeleteRule ctrlcommon.OnDeleteRule, +) []metav1.Condition { + ctx, _ = logx.WithSlog(ctx, logx.ObjectStore) + log := logx.GetSlog(ctx) + + var actualResource = &seaweedv1.Seaweed{} + conditions := make([]metav1.Condition, 0) + nsnBuilder := createNsNameBuilder(specNamespacedName) + + found, err := ctrlcommon.GetResource( + ctx, k8sClient, nsnBuilder.SpecNsName(), ResourceTypeName, actualResource, + ) + if err != nil { + return []metav1.Condition{ + { + Type: SeaweedCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + } + } + if !found { + actualResource = nil + if onDeleteRule.Policy == ctrlcommon.Purge { + log.Debug( + "Attempting to purge associated seaweedfs resources after deletion", + "seaweedName", SeaweedName(specNamespacedName.Name), + ) + err = purgeAssociatedResources(ctx, k8sClient, specNamespacedName.Namespace, onDeleteRule.Selector) + if err != nil { + conditions = append( + conditions, + metav1.Condition{ + Type: SeaweedCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + ) + } else { + conditions = append(conditions, metav1.Condition{ + Type: SeaweedReportedReadyType, + Status: metav1.ConditionFalse, + Reason: ctrlcommon.PendingDeleteReason, + }, + ) + } + } + } + + if actualResource != nil { + readyConditions := computeSeaweedReportedReadyCondition(ctx, actualResource) + conditions = append(conditions, readyConditions...) + if readyConditions[0].Status == metav1.ConditionTrue { + conditions = append(conditions, computeSeaweedWritableCondition(ctx, actualResource)) + conditions = append(conditions, computeSeaweedS3ReachableCondition(ctx, actualResource)) + } + } + log.Debug("read", "resourceExists", actualResource != nil, "rule", onDeleteRule.Policy) + return conditions +} + +type seaweedAssignResponse struct { + FID string `json:"fid"` + Error string `json:"error"` +} + +func computeSeaweedWritableCondition(ctx context.Context, cr *seaweedv1.Seaweed) metav1.Condition { + scheme := "http" + transport := http.DefaultTransport + if cr.Spec.TLS != nil && cr.Spec.TLS.Enabled { + scheme = "https" + tlsTransport := http.DefaultTransport.(*http.Transport).Clone() + // The Seaweed operator generates an internal certificate whose CA is not + // mounted into this controller. The request never leaves the cluster DNS + // name and only verifies that the master can allocate writable storage. + tlsTransport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12, InsecureSkipVerify: true} // #nosec G402 + transport = tlsTransport + } + + endpoint := url.URL{ + Scheme: scheme, + Host: fmt.Sprintf( + "%s-master.%s.svc.cluster.local:%d", + cr.Name, + cr.Namespace, + seaweedv1.MasterHTTPPort, + ), + Path: "/dir/assign", + } + query := endpoint.Query() + query.Set("count", "1") + if cr.Spec.Master != nil && cr.Spec.Master.DefaultReplication != nil { + query.Set("replication", *cr.Spec.Master.DefaultReplication) + } + endpoint.RawQuery = query.Encode() + + return probeSeaweedAllocation(ctx, &http.Client{Transport: transport, Timeout: seaweedProbeTimeout}, endpoint.String()) +} + +func computeSeaweedS3ReachableCondition(ctx context.Context, cr *seaweedv1.Seaweed) metav1.Condition { + scheme := "http" + transport := http.DefaultTransport + if cr.Spec.TLS != nil && cr.Spec.TLS.Enabled { + scheme = "https" + tlsTransport := http.DefaultTransport.(*http.Transport).Clone() + // The generated internal CA is not mounted into this controller. This + // unauthenticated probe stays on the cluster-local service address. + tlsTransport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12, InsecureSkipVerify: true} // #nosec G402 + transport = tlsTransport + } + endpoint := url.URL{ + Scheme: scheme, + Host: fmt.Sprintf("%s-s3.%s.svc.cluster.local:%s", cr.Name, cr.Namespace, S3Port), + Path: "/", + } + return probeSeaweedS3(ctx, &http.Client{Transport: transport, Timeout: seaweedProbeTimeout}, endpoint.String()) +} + +func probeSeaweedS3(ctx context.Context, client *http.Client, endpoint string) metav1.Condition { + condition := metav1.Condition{Type: SeaweedS3ReachableType, Status: metav1.ConditionFalse, Reason: "EndpointUnavailable"} + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + condition.Message = err.Error() + return condition + } + response, err := client.Do(req) + if err != nil { + condition.Message = err.Error() + return condition + } + defer response.Body.Close() + // An unauthenticated S3 service commonly returns 403. Any non-5xx response + // proves cluster DNS resolved and the S3 API accepted the connection. + if response.StatusCode >= http.StatusInternalServerError { + condition.Message = fmt.Sprintf("S3 endpoint returned HTTP %d", response.StatusCode) + return condition + } + condition.Status = metav1.ConditionTrue + condition.Reason = "EndpointReachable" + return condition +} + +func probeSeaweedAllocation(ctx context.Context, client *http.Client, endpoint string) metav1.Condition { + condition := metav1.Condition{ + Type: SeaweedWritableType, + Status: metav1.ConditionFalse, + Reason: "AllocationFailed", + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + condition.Message = err.Error() + return condition + } + response, err := client.Do(req) + if err != nil { + condition.Message = err.Error() + return condition + } + defer response.Body.Close() + + body, err := io.ReadAll(io.LimitReader(response.Body, 64*1024)) + if err != nil { + condition.Message = err.Error() + return condition + } + var assign seaweedAssignResponse + if err := json.Unmarshal(body, &assign); err != nil { + condition.Message = fmt.Sprintf("invalid allocation response: %v", err) + return condition + } + if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { + condition.Message = fmt.Sprintf("allocation returned HTTP %d: %s", response.StatusCode, assign.Error) + return condition + } + if assign.Error != "" { + condition.Message = assign.Error + return condition + } + if assign.FID == "" { + condition.Message = "allocation response did not include a file ID" + return condition + } + + condition.Status = metav1.ConditionTrue + condition.Reason = "AllocationSucceeded" + return condition +} + +func computeSeaweedReportedReadyCondition(_ context.Context, cr *seaweedv1.Seaweed) []metav1.Condition { + if cr == nil { + return []metav1.Condition{} + } + + allReady := true + anyRunning := false + + components := []struct { + name string + status seaweedv1.ComponentStatus + }{ + {"master", cr.Status.Master}, + {"volume", cr.Status.Volume}, + {"filer", cr.Status.Filer}, + } + + for _, c := range components { + if c.status.Replicas == 0 { + continue + } + if c.status.ReadyReplicas > 0 { + anyRunning = true + } + if c.status.ReadyReplicas < c.status.Replicas { + allReady = false + } + } + + var status metav1.ConditionStatus + var reason string + + switch { + case cr.Status.Filer.Replicas > 0 && cr.Status.Filer.ReadyReplicas == 0: + status = metav1.ConditionFalse + reason = "red" + case allReady && anyRunning: + status = metav1.ConditionTrue + reason = "green" + case anyRunning: + status = metav1.ConditionFalse + reason = "yellow" + default: + status = metav1.ConditionUnknown + reason = ctrlcommon.UnknownReason + } + + return []metav1.Condition{ + { + Type: SeaweedReportedReadyType, + Status: status, + Reason: reason, + }, + } +} diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/read_test.go b/internal/controller/infra/managed/objectstore/seaweedfs/read_test.go new file mode 100644 index 00000000..b92239fb --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/read_test.go @@ -0,0 +1,81 @@ +package seaweedfs + +import ( + "context" + "net/http" + "net/http/httptest" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("SeaweedFS writable storage probe", func() { + It("reports writable storage after an allocation succeeds", func() { + server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { + Expect(request.URL.Path).To(Equal("/dir/assign")) + _, err := response.Write([]byte(`{"fid":"3,01637037d6","url":"volume:8444","count":1}`)) + Expect(err).NotTo(HaveOccurred()) + })) + DeferCleanup(server.Close) + + condition := probeSeaweedAllocation(context.Background(), server.Client(), server.URL+"/dir/assign") + + Expect(condition.Type).To(Equal(SeaweedWritableType)) + Expect(condition.Status).To(Equal(metav1.ConditionTrue)) + Expect(condition.Reason).To(Equal("AllocationSucceeded")) + }) + + It("reports allocation errors as not writable", func() { + server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) { + response.WriteHeader(http.StatusInternalServerError) + _, err := response.Write([]byte(`{"error":"No writable volumes and no free volumes left"}`)) + Expect(err).NotTo(HaveOccurred()) + })) + DeferCleanup(server.Close) + + condition := probeSeaweedAllocation(context.Background(), server.Client(), server.URL) + + Expect(condition.Status).To(Equal(metav1.ConditionFalse)) + Expect(condition.Reason).To(Equal("AllocationFailed")) + Expect(condition.Message).To(ContainSubstring("No writable volumes")) + }) + + It("rejects successful responses without an allocation", func() { + server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) { + _, err := response.Write([]byte(`{"count":0}`)) + Expect(err).NotTo(HaveOccurred()) + })) + DeferCleanup(server.Close) + + condition := probeSeaweedAllocation(context.Background(), server.Client(), server.URL) + + Expect(condition.Status).To(Equal(metav1.ConditionFalse)) + Expect(condition.Message).To(ContainSubstring("file ID")) + }) + + It("reports an unauthenticated S3 API response as reachable", func() { + server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) { + response.WriteHeader(http.StatusForbidden) + })) + DeferCleanup(server.Close) + + condition := probeSeaweedS3(context.Background(), server.Client(), server.URL) + + Expect(condition.Type).To(Equal(SeaweedS3ReachableType)) + Expect(condition.Status).To(Equal(metav1.ConditionTrue)) + Expect(condition.Reason).To(Equal("EndpointReachable")) + }) + + It("rejects an unavailable S3 API", func() { + server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) { + response.WriteHeader(http.StatusServiceUnavailable) + })) + DeferCleanup(server.Close) + + condition := probeSeaweedS3(context.Background(), server.Client(), server.URL) + + Expect(condition.Status).To(Equal(metav1.ConditionFalse)) + Expect(condition.Reason).To(Equal("EndpointUnavailable")) + }) +}) diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/seaweedfs_suite_test.go b/internal/controller/infra/managed/objectstore/seaweedfs/seaweedfs_suite_test.go new file mode 100644 index 00000000..75131a3d --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/seaweedfs_suite_test.go @@ -0,0 +1,13 @@ +package seaweedfs + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestSeaweedFS(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "SeaweedFS Suite") +} diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/spec.go b/internal/controller/infra/managed/objectstore/seaweedfs/spec.go new file mode 100644 index 00000000..e98b7436 --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/spec.go @@ -0,0 +1,258 @@ +package seaweedfs + +import ( + "context" + "fmt" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + seaweedv1 "github.com/wandb/operator/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1" + "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" +) + +const ( + ObjectStoreModuleName = "seaweedfs" + + // TODO: remove this hardcoded default once all supported manifest versions + // supply bucket.<instance>.images.seaweedfs. + defaultSeaweedImage = "chrislusf/seaweedfs:4.35" +) + +func SeaweedImage(img manifest.ImageRef, globalImageRegistry string) string { + if out := img.GetImage(globalImageRegistry); out != "" { + return out + } + // Fallback for older manifests that don't supply the image. + return defaultSeaweedImage +} + +const ( + seaweedWritableTmpVolumeName = "seaweedfs-tmp" + seaweedWritableTmpMountPath = "/tmp" + seaweedFilerDataMountPath = "/data/filerldb2" + + // Filer holds only the leveldb2 path index; its size scales with object count, + // not total data. 20Gi is a safe default for large counts; override with + // FilerStorageSize when a deployment needs more. + seaweedFilerStorageSize = "20Gi" +) + +const ( + seaweedMasterMetricsPort int32 = 9091 + seaweedVolumeMetricsPort int32 = 9092 + seaweedFilerMetricsPort int32 = 9093 + seaweedVolumeSizeLimitMB int64 = 1024 +) + +func volumeLayout(storageQuantity resource.Quantity) (int32, int32) { + storageMB := storageQuantity.Value() / (1024 * 1024) + volumeSizeMB := min(seaweedVolumeSizeLimitMB, storageMB/2) + if volumeSizeMB < 1 { + volumeSizeMB = 1 + } + + maxVolumeCount := storageMB/volumeSizeMB - 1 + if maxVolumeCount < 1 { + maxVolumeCount = 1 + } + + return int32(volumeSizeMB), int32(maxVolumeCount) +} + +func seaweedWritableVolumes() []corev1.Volume { + return []corev1.Volume{ + { + Name: seaweedWritableTmpVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + } +} + +func seaweedWritableVolumeMounts() []corev1.VolumeMount { + return []corev1.VolumeMount{ + {Name: seaweedWritableTmpVolumeName, MountPath: seaweedWritableTmpMountPath}, + } +} + +func ToObjectStoreVendorSpec( + ctx context.Context, + wandb *apiv2.WeightsAndBiases, + infraSpec *apiv2.ManagedObjectStoreSpec, + scheme *runtime.Scheme, + mfst manifest.Manifest, +) (*seaweedv1.Seaweed, error) { + _, log := logx.WithSlog(ctx, logx.ObjectStore) + if infraSpec == nil { + return nil, nil + } + + specName := infraSpec.Name + + storageQuantity, err := resource.ParseQuantity(infraSpec.StorageSize) + if err != nil { + return nil, fmt.Errorf("invalid storage size %q: %w", infraSpec.StorageSize, err) + } + + replication := seaweedReplication(infraSpec.Copies, infraSpec.Replicas) + + volumeSizeLimitMB, maxVolumeCount := volumeLayout(storageQuantity) + + // Merge the storage request (PVC size) with any configured cpu/memory so neither drops the other. + volumeRequests := corev1.ResourceList{corev1.ResourceStorage: storageQuantity} + for name, qty := range infraSpec.Config.Resources.Requests { + volumeRequests[name] = qty + } + + filerStorageSize := seaweedFilerStorageSize + if infraSpec.SeaweedObjectStoreSpec.FilerStorageSize != "" { + filerStorageSize = infraSpec.SeaweedObjectStoreSpec.FilerStorageSize + } + filerStorageQuantity, err := resource.ParseQuantity(filerStorageSize) + if err != nil { + return nil, fmt.Errorf("invalid filer storage size %q: %w", filerStorageSize, err) + } + + labels := BuildWandbObjectStoreLabels(wandb) + labels["app"] = SeaweedName(specName) + + seaweedCR := &seaweedv1.Seaweed{ + ObjectMeta: metav1.ObjectMeta{ + Name: SeaweedName(specName), + Namespace: infraSpec.Namespace, + Labels: labels, + }, + Spec: seaweedv1.SeaweedSpec{ + Image: SeaweedImage(mfst.Bucket["default"].Images["seaweedfs"], wandb.Spec.Global.ImageRegistry), + TLS: &seaweedv1.TLSSpec{ + Enabled: infraSpec.SeaweedObjectStoreSpec.TlsEnabled, + }, + Master: &seaweedv1.MasterSpec{ + Replicas: 1, + DefaultReplication: &replication, + VolumeSizeLimitMB: &volumeSizeLimitMB, + MetricsPort: ptr.To(seaweedMasterMetricsPort), + ComponentSpec: seaweedv1.ComponentSpec{ + Volumes: seaweedWritableVolumes(), + VolumeMounts: seaweedWritableVolumeMounts(), + ExtraArgs: []string{"-ip.bind=0.0.0.0"}, + }, + }, + Volume: &seaweedv1.VolumeSpec{ + Replicas: infraSpec.Replicas, + VolumeServerConfig: seaweedv1.VolumeServerConfig{ + MetricsPort: ptr.To(seaweedVolumeMetricsPort), + MaxVolumeCounts: ptr.To(maxVolumeCount), + ComponentSpec: seaweedv1.ComponentSpec{ + Volumes: seaweedWritableVolumes(), + VolumeMounts: seaweedWritableVolumeMounts(), + ExtraArgs: []string{"-ip.bind=0.0.0.0"}, + }, + // Operator sizes the data PVC from Requests[storage] — a persistent disk, not ephemeral. + ResourceRequirements: corev1.ResourceRequirements{ + Requests: volumeRequests, + Limits: infraSpec.Config.Resources.Limits, + }, + }, + }, + S3: &seaweedv1.S3GatewaySpec{ + ComponentSpec: seaweedv1.ComponentSpec{ + Affinity: wandb.GetAffinity(infraSpec.ManagedInfraSpec), + Tolerations: *wandb.GetTolerations(infraSpec.ManagedInfraSpec), + Env: []corev1.EnvVar{{ + // W&B presigns S3 URLs against the in-cluster endpoint and + // rewrites the host for external clients without re-signing; + // pin signature verification to that endpoint so presigned + // requests arriving through an ingress proxy (whose + // Host/X-Forwarded-Host is the external hostname) validate. + Name: "S3_EXTERNAL_URL", + Value: s3ExternalURL(specName, infraSpec.Namespace, infraSpec.SeaweedObjectStoreSpec.TlsEnabled), + }}, + }, + ResourceRequirements: corev1.ResourceRequirements{}, + Replicas: 1, + ConfigSecret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: ConfigName(specName), + }, + Key: "config.json", + }, + DomainName: nil, + }, + Filer: &seaweedv1.FilerSpec{ + Replicas: 1, + MetricsPort: ptr.To(seaweedFilerMetricsPort), + Config: ptr.To("[leveldb2]\nenabled = true\ndir = \"" + seaweedFilerDataMountPath + "\""), + ComponentSpec: seaweedv1.ComponentSpec{ + Volumes: seaweedWritableVolumes(), + VolumeMounts: seaweedWritableVolumeMounts(), + ExtraArgs: []string{"-ip.bind=0.0.0.0"}, + }, + Persistence: &seaweedv1.PersistenceSpec{ + Enabled: true, + MountPath: ptr.To(seaweedFilerDataMountPath), + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: filerStorageQuantity, + }, + }, + }, + }, + Affinity: wandb.GetAffinity(infraSpec.ManagedInfraSpec), + Tolerations: *wandb.GetTolerations(infraSpec.ManagedInfraSpec), + }, + } + + if err := ctrl.SetControllerReference(wandb, seaweedCR, scheme); err != nil { + log.Error("failed to set owner reference on Seaweed CR", logx.ErrAttr(err)) + return nil, fmt.Errorf("failed to set owner reference: %w", err) + } + + return seaweedCR, nil +} + +// seaweedReplication builds the SeaweedFS replication code from the neutral copy +// count, clamped to the data-node count so we never request more copies than servers. +func seaweedReplication(copies, replicas int32) string { + // Unset copies keeps the legacy behavior: one extra copy once there is more than one server. + if copies <= 0 { + if replicas > 1 { + return "001" + } + return "000" + } + // Never request more copies than there are other servers to hold them. + maxCopies := replicas - 1 + if maxCopies < 0 { + maxCopies = 0 + } + if copies > maxCopies { + copies = maxCopies + } + return fmt.Sprintf("00%d", copies) +} + +func ToObjectStoreEnvConfig( + ctx context.Context, + spec apiv2.ManagedObjectStoreSpec, +) (SeaweedS3Config, error) { + return SeaweedS3Config{ + AccessKey: spec.Config.AccessKey, + }, nil +} + +func BuildWandbObjectStoreLabels(wandb *apiv2.WeightsAndBiases) map[string]string { + return common.BuildWandbLabels(wandb, ObjectStoreModuleName) +} + +func ToObjectStoreOnDeleteRule(wandb *apiv2.WeightsAndBiases, retentionPolicy apiv2.RetentionPolicy) common.OnDeleteRule { + return common.ToOnDeleteRule(wandb, retentionPolicy, ObjectStoreModuleName) +} diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/spec_test.go b/internal/controller/infra/managed/objectstore/seaweedfs/spec_test.go new file mode 100644 index 00000000..d90161a6 --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/spec_test.go @@ -0,0 +1,352 @@ +package seaweedfs + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + seaweedv1 "github.com/wandb/operator/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1" + "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ = Describe("SeaweedFS vendor specs", func() { + It("renders writable runtime mounts for SeaweedFS components", func() { + wandb := seaweedWandb() + + seaweed, err := ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed).NotTo(BeNil()) + + Expect(seaweed.Name).To(Equal(SeaweedName("object-store"))) + Expect(seaweed.Namespace).To(Equal("wandb")) + Expect(seaweed.Labels).To(HaveKeyWithValue("app", SeaweedName("object-store"))) + Expect(seaweed.Spec.Image).To(Equal(SeaweedImage(manifest.ImageRef{}, ""))) + + expectSeaweedWritableVolume(seaweed.Spec.Master.Volumes) + expectSeaweedWritableMount(seaweed.Spec.Master.VolumeMounts) + expectSeaweedWritableVolume(seaweed.Spec.Volume.Volumes) + expectSeaweedWritableMount(seaweed.Spec.Volume.VolumeMounts) + expectSeaweedWritableVolume(seaweed.Spec.Filer.Volumes) + expectSeaweedWritableMount(seaweed.Spec.Filer.VolumeMounts) + Expect(seaweed.Spec.Master.ExtraArgs).To(ContainElement("-ip.bind=0.0.0.0")) + Expect(seaweed.Spec.Volume.ExtraArgs).To(ContainElement("-ip.bind=0.0.0.0")) + Expect(seaweed.Spec.Filer.ExtraArgs).To(ContainElement("-ip.bind=0.0.0.0")) + }) + + It("retargets the image to spec.global.imageRegistry when set", func() { + wandb := seaweedWandb() + wandb.Spec.Global.ImageRegistry = "reg.corp:5000" + + mfst := manifest.Manifest{ + Bucket: map[string]manifest.InfraConfig{ + "default": { + Images: map[string]manifest.ImageRef{ + "seaweedfs": {Repository: "chrislusf/seaweedfs", Tag: "latest"}, + }, + }, + }, + } + + seaweed, err := ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), mfst) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed).NotTo(BeNil()) + Expect(seaweed.Spec.Image).To(Equal("reg.corp:5000/chrislusf/seaweedfs:latest")) + }) + + It("keeps the filer writable data path explicit", func() { + wandb := seaweedWandb() + seaweed, err := ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed).NotTo(BeNil()) + Expect(seaweed.Spec.Filer.Config).NotTo(BeNil()) + Expect(*seaweed.Spec.Filer.Config).To(ContainSubstring(`dir = "` + seaweedFilerDataMountPath + `"`)) + Expect(seaweed.Spec.Filer.Persistence).NotTo(BeNil()) + Expect(seaweed.Spec.Filer.Persistence.MountPath).NotTo(BeNil()) + Expect(*seaweed.Spec.Filer.Persistence.MountPath).To(Equal(seaweedFilerDataMountPath)) + }) + + It("preserves managed resource overrides", func() { + wandb := seaweedWandb() + seaweed, err := ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed).NotTo(BeNil()) + Expect(seaweed.Spec.Volume.ResourceRequirements.Requests[corev1.ResourceCPU]).To(Equal(resource.MustParse("500m"))) + }) + + It("reserves storage headroom for writable volumes", func() { + wandb := seaweedWandb() + seaweed, err := ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + + Expect(seaweed.Spec.Master.VolumeSizeLimitMB).NotTo(BeNil()) + Expect(*seaweed.Spec.Master.VolumeSizeLimitMB).To(Equal(int32(1024))) + Expect(seaweed.Spec.Volume.MaxVolumeCounts).NotTo(BeNil()) + Expect(*seaweed.Spec.Volume.MaxVolumeCounts).To(Equal(int32(9))) + }) + + DescribeTable("computes a writable volume layout", + func(storage string, expectedSizeMB, expectedMaxVolumes int32) { + size, count := volumeLayout(resource.MustParse(storage)) + Expect(size).To(Equal(expectedSizeMB)) + Expect(count).To(Equal(expectedMaxVolumes)) + }, + Entry("a development volume", "10Gi", int32(1024), int32(9)), + Entry("the upstream minimum example", "2Gi", int32(1024), int32(1)), + Entry("a sub-gibibyte volume", "512Mi", int32(256), int32(1)), + Entry("a large volume", "1Ti", int32(1024), int32(1023)), + ) + + It("pins s3 gateway signature verification to the in-cluster endpoint", func() { + wandb := seaweedWandb() + seaweed, err := ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed).NotTo(BeNil()) + Expect(seaweed.Spec.S3.Env).To(ContainElement(corev1.EnvVar{ + Name: "S3_EXTERNAL_URL", + Value: "http://" + SeaweedName("object-store") + "-s3.wandb.svc.cluster.local:" + S3Port, + })) + }) + + It("uses https for the s3 external URL when TLS is enabled", func() { + wandb := seaweedWandb() + wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.SeaweedObjectStoreSpec.TlsEnabled = true + + seaweed, err := ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed).NotTo(BeNil()) + Expect(seaweed.Spec.S3.Env).To(ContainElement(corev1.EnvVar{ + Name: "S3_EXTERNAL_URL", + Value: "https://" + SeaweedName("object-store") + "-s3.wandb.svc.cluster.local:" + S3Port, + })) + }) + + It("sets metrics ports on master, volume, and filer", func() { + wandb := seaweedWandb() + seaweed, err := ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed).NotTo(BeNil()) + + Expect(seaweed.Spec.Master.MetricsPort).NotTo(BeNil()) + Expect(*seaweed.Spec.Master.MetricsPort).To(Equal(seaweedMasterMetricsPort)) + + Expect(seaweed.Spec.Volume.MetricsPort).NotTo(BeNil()) + Expect(*seaweed.Spec.Volume.MetricsPort).To(Equal(seaweedVolumeMetricsPort)) + + Expect(seaweed.Spec.Filer.MetricsPort).NotTo(BeNil()) + Expect(*seaweed.Spec.Filer.MetricsPort).To(Equal(seaweedFilerMetricsPort)) + }) + + It("keeps the volume storage request when cpu/memory overrides are set", func() { + wandb := seaweedWandb() + seaweed, err := ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed).NotTo(BeNil()) + + requests := seaweed.Spec.Volume.Requests + Expect(requests[corev1.ResourceStorage]).To(Equal(resource.MustParse("10Gi"))) + Expect(requests[corev1.ResourceCPU]).To(Equal(resource.MustParse("500m"))) + }) + + It("sizes the filer disk independently of the data volumes", func() { + wandb := seaweedWandb() + seaweed, err := ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed).NotTo(BeNil()) + + filerStorage := seaweed.Spec.Filer.Persistence.Resources.Requests[corev1.ResourceStorage] + Expect(filerStorage).To(Equal(resource.MustParse(seaweedFilerStorageSize))) + Expect(filerStorage).NotTo(Equal(resource.MustParse("10Gi"))) + }) + + It("honors a configured filer storage size over the default", func() { + wandb := seaweedWandb() + wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.SeaweedObjectStoreSpec.FilerStorageSize = "50Gi" + seaweed, err := ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed).NotTo(BeNil()) + Expect(seaweed.Spec.Filer.Persistence.Resources.Requests[corev1.ResourceStorage]).To(Equal(resource.MustParse("50Gi"))) + }) +}) + +var _ = Describe("SeaweedFS translation edge cases", func() { + DescribeTable("maps replica count to a replication code", + func(replicas int32, wantReplication string) { + w := seaweedWandb() + w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.Replicas = replicas + seaweed, err := ToObjectStoreVendorSpec(context.Background(), w, w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed.Spec.Volume.Replicas).To(Equal(replicas)) + Expect(*seaweed.Spec.Master.DefaultReplication).To(Equal(wantReplication)) + }, + Entry("zero replicas", int32(0), "000"), + Entry("single replica", int32(1), "000"), + Entry("two replicas", int32(2), "001"), + Entry("three replicas", int32(3), "001"), + Entry("five replicas", int32(5), "001"), + ) + + DescribeTable("derives the replication code from an explicit copies count", + func(copies, replicas int32, wantReplication string) { + w := seaweedWandb() + w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.Copies = copies + w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.Replicas = replicas + seaweed, err := ToObjectStoreVendorSpec(context.Background(), w, w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(*seaweed.Spec.Master.DefaultReplication).To(Equal(wantReplication)) + }, + Entry("unset copies keeps a single copy on one node", int32(0), int32(1), "000"), + Entry("unset copies falls back to legacy one copy", int32(0), int32(3), "001"), + Entry("one extra copy", int32(1), int32(3), "001"), + Entry("two extra copies", int32(2), int32(3), "002"), + Entry("three extra copies", int32(3), int32(4), "003"), + Entry("copies clamped to data-node count", int32(5), int32(3), "002"), + Entry("copies clamped to zero on a single node", int32(2), int32(1), "000"), + Entry("one copy on two nodes", int32(1), int32(2), "001"), + Entry("negative copies treated as unset (never yields a bad code)", int32(-1), int32(3), "001"), + Entry("negative copies on a single node", int32(-3), int32(1), "000"), + ) + + It("layers cpu/memory requests and limits onto the volume without dropping storage", func() { + w := seaweedWandb() + w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.StorageSize = "100Gi" + w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.Config.Resources = corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("8Gi"), + }, + Limits: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("4")}, + } + seaweed, err := ToObjectStoreVendorSpec(context.Background(), w, w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + req := seaweed.Spec.Volume.Requests + Expect(req[corev1.ResourceStorage]).To(Equal(resource.MustParse("100Gi"))) + Expect(req[corev1.ResourceCPU]).To(Equal(resource.MustParse("2"))) + Expect(req[corev1.ResourceMemory]).To(Equal(resource.MustParse("8Gi"))) + Expect(seaweed.Spec.Volume.Limits[corev1.ResourceCPU]).To(Equal(resource.MustParse("4"))) + }) + + It("sets no cpu request and no limits when the CR configures none", func() { + w := seaweedWandb() + w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.Config.Resources = corev1.ResourceRequirements{} + seaweed, err := ToObjectStoreVendorSpec(context.Background(), w, w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed.Spec.Volume.Requests).To(HaveKey(corev1.ResourceStorage)) + Expect(seaweed.Spec.Volume.Requests).NotTo(HaveKey(corev1.ResourceCPU)) + Expect(seaweed.Spec.Volume.Limits).To(BeNil()) + }) + + DescribeTable("keeps the filer disk fixed regardless of data volume size", + func(storage string) { + w := seaweedWandb() + w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.StorageSize = storage + seaweed, err := ToObjectStoreVendorSpec(context.Background(), w, w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed.Spec.Volume.Requests[corev1.ResourceStorage]).To(Equal(resource.MustParse(storage))) + Expect(seaweed.Spec.Filer.Persistence.Resources.Requests[corev1.ResourceStorage]).To(Equal(resource.MustParse(seaweedFilerStorageSize))) + }, + Entry("small data disk", "10Gi"), + Entry("large data disk", "1Ti"), + ) + + DescribeTable("rejects an unparseable storage size", + func(storage string) { + w := seaweedWandb() + w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.StorageSize = storage + _, err := ToObjectStoreVendorSpec(context.Background(), w, w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).To(HaveOccurred()) + }, + Entry("empty", ""), + Entry("wrong unit", "10GB"), + Entry("garbage", "abc"), + ) + + It("returns nil when no managed object store is configured", func() { + w := seaweedWandb() + w.Spec.ObjectStore[apiv2.DefaultInstanceName] = apiv2.ObjectStoreSpec{} + seaweed, err := ToObjectStoreVendorSpec(context.Background(), w, w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed).To(BeNil()) + }) + + DescribeTable("propagates the TLS toggle", + func(tls bool) { + w := seaweedWandb() + w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.SeaweedObjectStoreSpec.TlsEnabled = tls + seaweed, err := ToObjectStoreVendorSpec(context.Background(), w, w.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed.Spec.TLS).NotTo(BeNil()) + Expect(seaweed.Spec.TLS.Enabled).To(Equal(tls)) + }, + Entry("enabled", true), + Entry("disabled", false), + ) +}) + +func seaweedScheme() *runtime.Scheme { + scheme := runtime.NewScheme() + Expect(apiv2.AddToScheme(scheme)).To(Succeed()) + Expect(seaweedv1.AddToScheme(scheme)).To(Succeed()) + return scheme +} + +func seaweedWandb() *apiv2.WeightsAndBiases { + tolerations := []corev1.Toleration{} + return &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiv2.GroupVersion.String(), + Kind: "WeightsAndBiases", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "wandb", + }, + Spec: apiv2.WeightsAndBiasesSpec{ + Tolerations: &tolerations, + ObjectStore: map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: { + ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{ + Name: "object-store", + Namespace: "wandb", + Replicas: 1, + StorageSize: "10Gi", + Config: apiv2.ObjectStoreConfig{ + AccessKey: "admin", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + }, + }, + }, + }, + }, + }, + }, + } +} + +func expectSeaweedWritableMount(mounts []corev1.VolumeMount) { + found := false + for _, mount := range mounts { + if mount.Name == seaweedWritableTmpVolumeName && mount.MountPath == seaweedWritableTmpMountPath { + found = true + break + } + } + Expect(found).To(BeTrue()) +} + +func expectSeaweedWritableVolume(volumes []corev1.Volume) { + found := false + for _, volume := range volumes { + if volume.Name == seaweedWritableTmpVolumeName && volume.EmptyDir != nil { + found = true + break + } + } + Expect(found).To(BeTrue()) +} diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/status.go b/internal/controller/infra/managed/objectstore/seaweedfs/status.go new file mode 100644 index 00000000..7053b7b3 --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/status.go @@ -0,0 +1,245 @@ +package seaweedfs + +import ( + "context" + "time" + + "github.com/samber/lo" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" +) + +const ( + SeaweedCustomResourceType = "SeaweedCustomResource" + SeaweedConnectionInfoType = "SeaweedConnectionInfo" + SeaweedReportedReadyType = "SeaweedReportedReady" + SeaweedWritableType = "SeaweedWritable" + SeaweedS3ReachableType = "SeaweedS3Reachable" +) + +func ComputeStatus( + ctx context.Context, + enabled bool, + oldConditions, currentConditions []metav1.Condition, + connection *apiv2.ObjectStoreConnection, + currentGeneration int64, +) (apiv2.ObjectStoreInfraStatus, []corev1.Event, ctrl.Result) { + ctx, _ = logx.WithSlog(ctx, logx.ObjectStore) + result := apiv2.ObjectStoreInfraStatus{} + + if connection != nil { + result.Connection = *connection + } + + currentConditions = applyDefaultConditions(currentConditions) + + result.Conditions = common.ComputeConditionUpdates( + oldConditions, + currentConditions, + currentGeneration, + common.DefaultConditionExpiry, + ) + + state, events := inferInfraState(ctx, enabled, result.Conditions) + result.State = state + + result.Ready = !lo.Contains(common.NotReadyStates, result.State) + + requeueAfter := 3 * time.Minute + switch result.State { + case common.ErrorState: + requeueAfter = 15 * time.Second + case common.DegradedState: + requeueAfter = 5 * time.Minute + case common.PendingState: + requeueAfter = 2 * time.Minute + case common.HealthyState: + requeueAfter = 10 * time.Minute + } + + return result, events, ctrl.Result{RequeueAfter: requeueAfter} +} + +func applyDefaultConditions(conditions []metav1.Condition) []metav1.Condition { + if !common.ContainsType(conditions, SeaweedConnectionInfoType) { + conditions = append(conditions, metav1.Condition{ + Type: SeaweedConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }) + } + if !common.ContainsType(conditions, SeaweedWritableType) { + conditions = append(conditions, metav1.Condition{ + Type: SeaweedWritableType, + Status: metav1.ConditionUnknown, + Reason: common.NoResourceReason, + }) + } + if !common.ContainsType(conditions, SeaweedS3ReachableType) { + conditions = append(conditions, metav1.Condition{ + Type: SeaweedS3ReachableType, + Status: metav1.ConditionUnknown, + Reason: common.NoResourceReason, + }) + } + + return conditions +} + +func inferInfraState( + ctx context.Context, + enabled bool, + conditions []metav1.Condition, +) (string, []corev1.Event) { + if !enabled { + return common.UnavailableState, nil + } + var events []corev1.Event + impliedStates := make(map[string]string, len(conditions)) + + impliedStates = inferStateFromCondition(ctx, SeaweedCustomResourceType, impliedStates, conditions) + impliedStates = inferStateFromCondition(ctx, SeaweedConnectionInfoType, impliedStates, conditions) + impliedStates = inferStateFromCondition(ctx, SeaweedReportedReadyType, impliedStates, conditions) + impliedStates = inferStateFromCondition(ctx, SeaweedWritableType, impliedStates, conditions) + impliedStates = inferStateFromCondition(ctx, SeaweedS3ReachableType, impliedStates, conditions) + + hasImpliedState := func(target string) bool { + return len(lo.FilterValues( + impliedStates, + func(_ string, value string) bool { + return value == target + })) > 0 + } + + summaryState := common.UnknownState + + if impliedStates[SeaweedConnectionInfoType] == common.UnavailableState && + impliedStates[SeaweedReportedReadyType] == common.HealthyState { + events = append(events, corev1.Event{ + Type: corev1.EventTypeWarning, + Reason: "SeaweedConnectionInfoUnavailable", + Message: "SeaweedFS connection info is unavailable, but SeaweedFS is reported as ready.", + }) + summaryState = common.ErrorState + } + + if summaryState != common.UnknownState { + return summaryState, events + } + + stateSignificanceOrder := []string{ + common.ErrorState, + common.UnavailableState, + common.PendingState, + common.DegradedState, + common.HealthyState, + } + for _, s := range stateSignificanceOrder { + if hasImpliedState(s) { + return s, events + } + } + + return common.UnknownState, events +} + +func inferStateFromCondition(ctx context.Context, conditionType string, impliedStates map[string]string, conditions []metav1.Condition) map[string]string { + cond, found := lo.Find(conditions, func(c metav1.Condition) bool { return c.Type == conditionType }) + if !found { + impliedStates[conditionType] = common.UnknownState + } else { + switch conditionType { + case SeaweedCustomResourceType: + impliedStates[conditionType] = inferState_SeaweedCustomResourceType(ctx, cond) + case SeaweedConnectionInfoType: + impliedStates[conditionType] = inferState_SeaweedConnectionInfoType(ctx, cond) + case SeaweedReportedReadyType: + impliedStates[conditionType] = inferState_SeaweedReportedReadyType(ctx, cond) + case SeaweedWritableType, SeaweedS3ReachableType: + impliedStates[conditionType] = inferState_SeaweedWritableType(ctx, cond) + default: + impliedStates[conditionType] = common.UnknownState + } + } + return impliedStates +} + +func inferState_SeaweedWritableType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.PendingState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + result = common.ErrorState + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} + +func inferState_SeaweedCustomResourceType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + if condition.Reason == common.PendingCreateReason { + result = common.PendingState + } + if condition.Reason == common.PendingDeleteReason { + result = common.UnavailableState + } + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} + +func inferState_SeaweedConnectionInfoType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + result = common.UnavailableState + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} + +func inferState_SeaweedReportedReadyType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + switch condition.Reason { + case "yellow": + result = common.DegradedState + case "red": + result = common.ErrorState + default: + result = common.DegradedState + } + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/status_test.go b/internal/controller/infra/managed/objectstore/seaweedfs/status_test.go new file mode 100644 index 00000000..cc6aeca4 --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/status_test.go @@ -0,0 +1,69 @@ +package seaweedfs + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/wandb/operator/internal/controller/common" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("SeaweedFS status", func() { + It("does not report healthy from component readiness alone", func() { + status, _, _ := ComputeStatus( + context.Background(), + true, + nil, + []metav1.Condition{ + {Type: SeaweedCustomResourceType, Status: metav1.ConditionTrue}, + {Type: SeaweedConnectionInfoType, Status: metav1.ConditionTrue}, + {Type: SeaweedReportedReadyType, Status: metav1.ConditionTrue}, + }, + nil, + 1, + ) + + Expect(status.Ready).To(BeFalse()) + Expect(status.State).To(Equal(common.PendingState)) + }) + + It("reports an allocation failure as an error", func() { + status, _, _ := ComputeStatus( + context.Background(), + true, + nil, + []metav1.Condition{ + {Type: SeaweedCustomResourceType, Status: metav1.ConditionTrue}, + {Type: SeaweedConnectionInfoType, Status: metav1.ConditionTrue}, + {Type: SeaweedReportedReadyType, Status: metav1.ConditionTrue}, + {Type: SeaweedWritableType, Status: metav1.ConditionFalse, Reason: "AllocationFailed"}, + }, + nil, + 1, + ) + + Expect(status.Ready).To(BeFalse()) + Expect(status.State).To(Equal(common.ErrorState)) + }) + + It("reports healthy only after allocation succeeds", func() { + status, _, _ := ComputeStatus( + context.Background(), + true, + nil, + []metav1.Condition{ + {Type: SeaweedCustomResourceType, Status: metav1.ConditionTrue}, + {Type: SeaweedConnectionInfoType, Status: metav1.ConditionTrue}, + {Type: SeaweedReportedReadyType, Status: metav1.ConditionTrue}, + {Type: SeaweedWritableType, Status: metav1.ConditionTrue, Reason: "AllocationSucceeded"}, + {Type: SeaweedS3ReachableType, Status: metav1.ConditionTrue, Reason: "EndpointReachable"}, + }, + nil, + 1, + ) + + Expect(status.Ready).To(BeTrue()) + Expect(status.State).To(Equal(common.HealthyState)) + }) +}) diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/write.go b/internal/controller/infra/managed/objectstore/seaweedfs/write.go new file mode 100644 index 00000000..6912c0ae --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/write.go @@ -0,0 +1,223 @@ +package seaweedfs + +import ( + "context" + "fmt" + + "github.com/Masterminds/goutils" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + seaweedv1 "github.com/wandb/operator/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + ResourceTypeName = "Seaweed" + ConfigTypeName = "SeaweedS3Config" + AppConnTypeName = "SeaweedAppConn" +) + +func WriteState( + ctx context.Context, + kubeClient client.Client, + specNamespacedName types.NamespacedName, + desiredCr *seaweedv1.Seaweed, + envConfig SeaweedS3Config, + wandbOwner client.Object, +) ([]metav1.Condition, *apiv2.ObjectStoreConnection) { + ctx, _ = logx.WithSlog(ctx, logx.ObjectStore) + var actual = &seaweedv1.Seaweed{} + + nsnBuilder := createNsNameBuilder(specNamespacedName) + + found, err := common.GetResource( + ctx, kubeClient, nsnBuilder.SpecNsName(), ResourceTypeName, actual, + ) + if err != nil { + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }, + { + Type: SeaweedCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: common.ApiErrorReason, + }, + }, nil + } + if !found { + actual = nil + } + + result := make([]metav1.Condition, 0) + + action, err := common.CrudResource(ctx, kubeClient, desiredCr, actual) + if err != nil { + result = append(result, metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }) + return result, nil + } + + switch action { + case common.CreateAction: + result = append(result, metav1.Condition{ + Type: SeaweedCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.PendingCreateReason, + }) + case common.DeleteAction: + result = append(result, metav1.Condition{ + Type: SeaweedCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.PendingDeleteReason, + }) + case common.UpdateAction, common.UnchangedAction: + result = append(result, metav1.Condition{ + Type: SeaweedCustomResourceType, + Status: metav1.ConditionTrue, + Reason: common.ResourceExistsReason, + }) + case common.NoAction: + result = append(result, metav1.Condition{ + Type: SeaweedCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }) + } + + connInfo, err := writeSeaweedS3Config( + ctx, kubeClient, desiredCr, nsnBuilder, envConfig, + ) + if err != nil { + result = append(result, metav1.Condition{ + Type: SeaweedConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }) + return result, nil + } + + if connInfo != nil { + connection, err := writeWandbConnInfo( + ctx, kubeClient, wandbOwner, nsnBuilder, connInfo, + ) + if err != nil { + result = append(result, metav1.Condition{ + Type: SeaweedConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }) + return result, nil + } + if connection != nil { + result = append(result, metav1.Condition{ + Type: SeaweedConnectionInfoType, + Status: metav1.ConditionTrue, + Reason: common.ResourceExistsReason, + }) + return result, connection + } + } + + result = append(result, metav1.Condition{ + Type: SeaweedConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }) + return result, nil +} + +func writeSeaweedS3Config( + ctx context.Context, + client client.Client, + owner *seaweedv1.Seaweed, + nsnBuilder *NsNameBuilder, + envConfig SeaweedS3Config, +) (*s3ConnInfo, error) { + var err error + var found bool + var gvk schema.GroupVersionKind + var secretKey string + var actual = &corev1.Secret{} + + configFileName := "config.json" + + if owner == nil { + return nil, nil + } + + if found, err = common.GetResource( + ctx, client, nsnBuilder.ConfigNsName(), ConfigTypeName, actual, + ); err != nil { + return nil, err + } + if !found { + actual = nil + } + + if actual != nil { + if raw, ok := actual.Data[configFileName]; ok { + existingConfig, parseError := parseS3IdentityConfig(string(raw)) + if parseError != nil { + return nil, fmt.Errorf("parse existing %s: %w", configFileName, parseError) + } + secretKey, err = extractSecretKey(existingConfig, envConfig.AccessKey) + if err != nil { + return nil, err + } + } + } + if secretKey == "" { + if secretKey, err = goutils.RandomAlphabetic(20); err != nil { + return nil, err + } + } + + identityConfig := buildS3IdentityConfig(envConfig.AccessKey, secretKey) + configJSON, err := identityConfig.toJSON() + if err != nil { + return nil, err + } + + if gvk, err = client.GroupVersionKindFor(owner); err != nil { + return nil, fmt.Errorf("could not get GVK for owner: %w", err) + } + ref := metav1.OwnerReference{ + APIVersion: gvk.GroupVersion().String(), + Kind: gvk.Kind, + Name: owner.GetName(), + UID: owner.GetUID(), + Controller: ptr.To(false), + BlockOwnerDeletion: ptr.To(false), + } + + desired := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsnBuilder.ConfigName(), + Namespace: nsnBuilder.Namespace(), + OwnerReferences: []metav1.OwnerReference{ref}, + }, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{ + configFileName: configJSON, + }, + } + + if _, err = common.CrudResource(ctx, client, desired, actual); err != nil { + return nil, err + } + + tls := owner.Spec.TLS != nil && owner.Spec.TLS.Enabled + return buildS3ConnInfo(envConfig.AccessKey, secretKey, nsnBuilder, tls), nil +} diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/write_test.go b/internal/controller/infra/managed/objectstore/seaweedfs/write_test.go new file mode 100644 index 00000000..8d2f78bb --- /dev/null +++ b/internal/controller/infra/managed/objectstore/seaweedfs/write_test.go @@ -0,0 +1,87 @@ +package seaweedfs + +import ( + "context" + "errors" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + seaweedv1 "github.com/wandb/operator/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1" + "github.com/wandb/operator/pkg/wandb/manifest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" +) + +func writeScheme() *runtime.Scheme { + scheme := runtime.NewScheme() + Expect(clientgoscheme.AddToScheme(scheme)).To(Succeed()) + Expect(apiv2.AddToScheme(scheme)).To(Succeed()) + Expect(seaweedv1.AddToScheme(scheme)).To(Succeed()) + return scheme +} + +func hasCondition(conds []metav1.Condition, condType string, status metav1.ConditionStatus) bool { + for _, c := range conds { + if c.Type == condType && c.Status == status { + return true + } + } + return false +} + +var _ = Describe("SeaweedFS WriteState", func() { + var ( + ctx context.Context + wandb *apiv2.WeightsAndBiases + desired *seaweedv1.Seaweed + envCfg SeaweedS3Config + specNsn types.NamespacedName + errWrite = errors.New("boom: apiserver write failed") + ) + + BeforeEach(func() { + ctx = context.Background() + wandb = seaweedWandb() + var err error + desired, err = ToObjectStoreVendorSpec(ctx, wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, writeScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(desired).NotTo(BeNil()) + envCfg = SeaweedS3Config{AccessKey: "admin"} + specNsn = types.NamespacedName{Namespace: "wandb", Name: "object-store"} + }) + + It("returns early and never reports a healthy connection when the CR write fails", func() { + cl := fake.NewClientBuilder(). + WithScheme(writeScheme()). + WithInterceptorFuncs(interceptor.Funcs{ + Create: func(context.Context, client.WithWatch, client.Object, ...client.CreateOption) error { + return errWrite + }, + }). + Build() + + conds, conn := WriteState(ctx, cl, specNsn, desired, envCfg, wandb) + + Expect(conn).To(BeNil()) + Expect(hasCondition(conds, common.ReconciledType, metav1.ConditionFalse)).To(BeTrue()) + // A failed CR write must not fall through to a "connection ready" report. + Expect(hasCondition(conds, SeaweedConnectionInfoType, metav1.ConditionTrue)).To(BeFalse()) + }) + + It("reports pending-create and writes a connection when the CR write succeeds", func() { + cl := fake.NewClientBuilder().WithScheme(writeScheme()).Build() + + conds, conn := WriteState(ctx, cl, specNsn, desired, envCfg, wandb) + + Expect(hasCondition(conds, SeaweedCustomResourceType, metav1.ConditionFalse)).To(BeTrue()) + Expect(conn).NotTo(BeNil()) + Expect(hasCondition(conds, SeaweedConnectionInfoType, metav1.ConditionTrue)).To(BeTrue()) + }) +}) diff --git a/internal/controller/infra/managed/redis/opstree/conn.go b/internal/controller/infra/managed/redis/opstree/conn.go new file mode 100644 index 00000000..d527ef14 --- /dev/null +++ b/internal/controller/infra/managed/redis/opstree/conn.go @@ -0,0 +1,102 @@ +package opstree + +import ( + "context" + "fmt" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + redisv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redis/v1beta2" + redissentinelv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redissentinel/v1beta2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func readStandaloneConnectionDetails(standaloneActual *redisv1beta2.Redis) *redisConnInfo { + redisHost := fmt.Sprintf("%s.%s.svc.cluster.local", standaloneActual.Name, standaloneActual.GetNamespace()) + redisPort := "6379" + + return &redisConnInfo{ + Host: redisHost, + Port: redisPort, + } +} + +func readSentinelConnectionDetails(sentinelActual *redissentinelv1beta2.RedisSentinel) *redisConnInfo { + sentinelHost := fmt.Sprintf("%s-sentinel.%s.svc.cluster.local", sentinelActual.Name, sentinelActual.GetNamespace()) + sentinelPort := "26379" + masterName := "gorilla" + + return &redisConnInfo{ + SentinelHost: sentinelHost, + SentinelPort: sentinelPort, + SentinelMaster: masterName, + } +} + +func writeRedisConnInfo( + ctx context.Context, + client client.Client, + owner client.Object, + nsnBuilder *NsNameBuilder, + connInfo *redisConnInfo, +) ( + *apiv2.RedisConnection, error, +) { + var err error + var found bool + var gvk schema.GroupVersionKind + var actual = &corev1.Secret{} + + nsName := nsnBuilder.ConnectionNsName() + urlKey := "url" + + if found, err = common.GetResource( + ctx, client, nsName, AppConnTypeName, actual, + ); err != nil { + return nil, err + } + if !found { + actual = nil + } + + if gvk, err = client.GroupVersionKindFor(owner); err != nil { + return nil, fmt.Errorf("could not get GVK for owner: %w", err) + } + ref := metav1.OwnerReference{ + APIVersion: gvk.GroupVersion().String(), + Kind: gvk.Kind, + Name: owner.GetName(), + UID: owner.GetUID(), + Controller: ptr.To(false), + BlockOwnerDeletion: ptr.To(false), + } + + desired := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsName.Name, + Namespace: nsName.Namespace, + OwnerReferences: []metav1.OwnerReference{ref}, + }, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{ + urlKey: connInfo.toURL(), + "Host": connInfo.Host, + "Port": connInfo.Port, + }, + } + + if _, err = common.CrudResource(ctx, client, desired, actual); err != nil { + return nil, err + } + + localRef := corev1.LocalObjectReference{Name: nsName.Name} + return &apiv2.RedisConnection{ + URL: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: urlKey, Optional: ptr.To(false)}, + Host: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Host", Optional: ptr.To(false)}, + Port: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Port", Optional: ptr.To(false)}, + }, nil +} diff --git a/internal/controller/infra/managed/redis/opstree/detach.go b/internal/controller/infra/managed/redis/opstree/detach.go new file mode 100644 index 00000000..420cb5ed --- /dev/null +++ b/internal/controller/infra/managed/redis/opstree/detach.go @@ -0,0 +1,90 @@ +package opstree + +import ( + "context" + "log/slog" + + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + redisv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redis/v1beta2" + redisreplicationv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redisreplication/v1beta2" + redissentinelv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redissentinel/v1beta2" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func CheckDetached( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + wandbUID types.UID, +) []metav1.Condition { + nsnBuilder := createNsNameBuilder(specNamespacedName) + actual := &redisv1beta2.Redis{} + found, err := common.GetResource(ctx, cl, nsnBuilder.StandaloneNsName(), StandaloneType, actual) + if err != nil || !found { + return nil + } + if !common.IsDetached(actual, wandbUID) { + return nil + } + return nil +} + +func DetachFinalizer( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + wandbOwner client.Object, +) error { + ctx, log := logx.WithSlog(ctx, logx.Redis) + nsnBuilder := createNsNameBuilder(specNamespacedName) + + if err := detachResource(ctx, cl, log, nsnBuilder.StandaloneNsName(), StandaloneType, &redisv1beta2.Redis{}, wandbOwner); err != nil { + return err + } + if err := detachResource(ctx, cl, log, nsnBuilder.SentinelNsName(), SentinelType, &redissentinelv1beta2.RedisSentinel{}, wandbOwner); err != nil { + return err + } + if err := detachResource(ctx, cl, log, nsnBuilder.ReplicationNsName(), ReplicationType, &redisreplicationv1beta2.RedisReplication{}, wandbOwner); err != nil { + return err + } + return detachResource(ctx, cl, log, nsnBuilder.ConnectionNsName(), "Secret", &corev1.Secret{}, wandbOwner) +} + +func detachResource[T client.Object]( + ctx context.Context, + cl client.Client, + log *slog.Logger, + nsName types.NamespacedName, + typeName string, + actual T, + wandbOwner client.Object, +) error { + found, err := common.GetResource(ctx, cl, nsName, typeName, actual) + if err != nil { + return err + } + if !found { + log.Info("abort detach: CR not found", "type", typeName) + return nil + } + if common.IsDetached(actual, wandbOwner.GetUID()) { + log.Debug("CR already detached", "type", typeName) + return nil + } + + common.RemoveOwnerReference(actual, wandbOwner.GetUID()) + if err = cl.Update(ctx, actual); err != nil { + if errors.IsNotFound(err) { + return nil + } + log.Error("error detaching CR", logx.ErrAttr(err), "type", typeName) + return err + } + log.Info("detached CR", "type", typeName, "name", nsName.Name) + return nil +} diff --git a/internal/controller/infra/managed/redis/opstree/image_test.go b/internal/controller/infra/managed/redis/opstree/image_test.go new file mode 100644 index 00000000..55ca6b6b --- /dev/null +++ b/internal/controller/infra/managed/redis/opstree/image_test.go @@ -0,0 +1,76 @@ +package opstree + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/wandb/operator/pkg/wandb/manifest" +) + +var _ = Describe("RedisStandaloneImage", func() { + DescribeTable("resolves the image string", + func(img manifest.ImageRef, globalImageRegistry, expected string) { + Expect(RedisStandaloneImage(img, globalImageRegistry)).To(Equal(expected)) + }, + Entry("no manifest image", manifest.ImageRef{}, "", defaultRedisStandaloneImage), + Entry("manifest image", + manifest.ImageRef{Registry: "quay.io", Repository: "opstree/redis", Tag: "v7.0.15"}, + "", + "quay.io/opstree/redis:v7.0.15"), + Entry("manifest image, global registry set", + manifest.ImageRef{Registry: "quay.io", Repository: "opstree/redis", Tag: "v7.0.15"}, + "myregistry.io", + "myregistry.io/quay.io/opstree/redis:v7.0.15"), + ) +}) + +var _ = Describe("RedisReplicationImage", func() { + DescribeTable("resolves the image string", + func(img manifest.ImageRef, globalImageRegistry, expected string) { + Expect(RedisReplicationImage(img, globalImageRegistry)).To(Equal(expected)) + }, + Entry("no manifest image", manifest.ImageRef{}, "", defaultRedisReplicationImage), + Entry("manifest image", + manifest.ImageRef{Registry: "quay.io", Repository: "opstree/redis", Tag: "v7.0.15"}, + "", + "quay.io/opstree/redis:v7.0.15"), + Entry("manifest image, global registry set", + manifest.ImageRef{Registry: "quay.io", Repository: "opstree/redis", Tag: "v7.0.15"}, + "myregistry.io", + "myregistry.io/quay.io/opstree/redis:v7.0.15"), + ) +}) + +var _ = Describe("RedisSentinelImage", func() { + DescribeTable("resolves the image string", + func(img manifest.ImageRef, globalImageRegistry, expected string) { + Expect(RedisSentinelImage(img, globalImageRegistry)).To(Equal(expected)) + }, + Entry("no manifest image", manifest.ImageRef{}, "", defaultRedisSentinelImage), + Entry("manifest image", + manifest.ImageRef{Registry: "quay.io", Repository: "opstree/redis-sentinel", Tag: "v7.0.12"}, + "", + "quay.io/opstree/redis-sentinel:v7.0.12"), + Entry("manifest image, global registry set", + manifest.ImageRef{Registry: "quay.io", Repository: "opstree/redis-sentinel", Tag: "v7.0.12"}, + "myregistry.io", + "myregistry.io/quay.io/opstree/redis-sentinel:v7.0.12"), + ) +}) + +var _ = Describe("DefaultRedisExporterImage", func() { + DescribeTable("resolves the image string", + func(img manifest.ImageRef, globalImageRegistry, expected string) { + Expect(DefaultRedisExporterImage(img, globalImageRegistry)).To(Equal(expected)) + }, + Entry("no manifest image", manifest.ImageRef{}, "", defaultRedisExporterImage), + Entry("manifest image", + manifest.ImageRef{Registry: "quay.io", Repository: "opstree/redis-exporter", Tag: "v1.44.0"}, + "", + "quay.io/opstree/redis-exporter:v1.44.0"), + Entry("manifest image, global registry set", + manifest.ImageRef{Registry: "quay.io", Repository: "opstree/redis-exporter", Tag: "v1.44.0"}, + "myregistry.io", + "myregistry.io/quay.io/opstree/redis-exporter:v1.44.0"), + ) +}) diff --git a/internal/controller/infra/managed/redis/opstree/naming.go b/internal/controller/infra/managed/redis/opstree/naming.go new file mode 100644 index 00000000..5075de0e --- /dev/null +++ b/internal/controller/infra/managed/redis/opstree/naming.go @@ -0,0 +1,92 @@ +package opstree + +import ( + "fmt" + + "github.com/wandb/operator/internal/controller/common" + "k8s.io/apimachinery/pkg/types" +) + +// MaxSpecNameLength is a conservative budget: this package and the opstree +// operator suffix the spec name ("-replica", "-sentinel", "-headless", ...); +// 40 leaves 23 chars of DNS-1123 label headroom. +const MaxSpecNameLength = 40 + +const defaultNameSuffix = "-redis" + +// DefaultSpecName derives the managed Redis name for a CR instance, shortened +// to the budget. +func DefaultSpecName(crName, instanceKey string) string { + return common.FitDefaultInfraName(common.InstanceBaseName(crName, instanceKey), defaultNameSuffix, MaxSpecNameLength) +} + +type NsNameBuilder struct { + baseNsName types.NamespacedName +} + +func CreateNsNameBuilder(baseNsName types.NamespacedName) *NsNameBuilder { + return &NsNameBuilder{ + baseNsName: baseNsName, + } +} + +func (n *NsNameBuilder) Namespace() string { + return n.baseNsName.Namespace +} + +func (n *NsNameBuilder) SpecName() string { + return n.baseNsName.Name +} + +func (n *NsNameBuilder) StandaloneName() string { + return n.SpecName() +} + +func (n *NsNameBuilder) StandaloneNamespace() string { + return n.Namespace() +} + +func (n *NsNameBuilder) StandaloneNsName() types.NamespacedName { + return types.NamespacedName{ + Namespace: n.Namespace(), + Name: n.StandaloneName(), + } +} + +func (n *NsNameBuilder) SentinelName() string { + return n.SpecName() +} + +func (n *NsNameBuilder) SentinelNsName() types.NamespacedName { + return types.NamespacedName{ + Namespace: n.Namespace(), + Name: n.SentinelName(), + } +} + +func (n *NsNameBuilder) ReplicationName() string { + return fmt.Sprintf("%s-replica", n.SpecName()) +} + +func (n *NsNameBuilder) ReplicationNsName() types.NamespacedName { + return types.NamespacedName{ + Namespace: n.Namespace(), + Name: n.ReplicationName(), + } +} + +func (n *NsNameBuilder) ConnectionName() string { + return fmt.Sprintf("%s-connection", n.SpecName()) +} + +func (n *NsNameBuilder) ConnectionNsName() types.NamespacedName { + return types.NamespacedName{ + Namespace: n.Namespace(), + Name: n.ConnectionName(), + } +} + +// Internal function for backward compatibility within the package +func createNsNameBuilder(baseNsName types.NamespacedName) *NsNameBuilder { + return CreateNsNameBuilder(baseNsName) +} diff --git a/internal/controller/infra/managed/redis/opstree/opstree_suite_test.go b/internal/controller/infra/managed/redis/opstree/opstree_suite_test.go new file mode 100644 index 00000000..00ebbe89 --- /dev/null +++ b/internal/controller/infra/managed/redis/opstree/opstree_suite_test.go @@ -0,0 +1,13 @@ +package opstree + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestOpstree(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Opstree-Redis Suite") +} diff --git a/internal/controller/infra/managed/redis/opstree/purge.go b/internal/controller/infra/managed/redis/opstree/purge.go new file mode 100644 index 00000000..c753cc55 --- /dev/null +++ b/internal/controller/infra/managed/redis/opstree/purge.go @@ -0,0 +1,63 @@ +package opstree + +import ( + "context" + + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func PurgeFinalizer( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + onDeleteRule common.OnDeleteRule, +) error { + ctx, _ = logx.WithSlog(ctx, logx.Redis) + if onDeleteRule.Policy != common.Purge { + return nil + } + return purgeAssociatedResources(ctx, cl, specNamespacedName.Namespace, onDeleteRule.Selector) +} + +func purgeAssociatedResources( + ctx context.Context, + cl client.Client, + namespace string, + onDeleteSelector labels.Selector, +) error { + log := logx.GetSlog(ctx) + listOptions := &client.ListOptions{ + Namespace: namespace, + LabelSelector: onDeleteSelector, + } + + // PVCs + pvcList := &corev1.PersistentVolumeClaimList{} + if err := cl.List(ctx, pvcList, listOptions); err != nil { + return err + } + if len(pvcList.Items) > 0 { + log.Info( + "Purging associated PVCs", + "count", len(pvcList.Items), "selector", onDeleteSelector.String(), + ) + } else { + log.Debug( + "No associated PVCs found to purge", + "selector", onDeleteSelector.String(), + ) + } + for _, pvc := range pvcList.Items { + if err := cl.Delete(ctx, &pvc); err != nil && !errors.IsNotFound(err) { + return err + } + } + + return nil +} diff --git a/internal/controller/infra/managed/redis/opstree/read.go b/internal/controller/infra/managed/redis/opstree/read.go new file mode 100644 index 00000000..f981d009 --- /dev/null +++ b/internal/controller/infra/managed/redis/opstree/read.go @@ -0,0 +1,405 @@ +package opstree + +import ( + "context" + "fmt" + + apiv2 "github.com/wandb/operator/api/v2" + ctrlcommon "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + redisv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redis/v1beta2" + redisreplicationv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redisreplication/v1beta2" + redissentinelv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redissentinel/v1beta2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func ReadState( + ctx context.Context, + client client.Client, + specNamespacedName types.NamespacedName, + wandbOwner client.Object, + onDeleteRule ctrlcommon.OnDeleteRule, +) ([]metav1.Condition, *apiv2.RedisConnection) { + ctx, _ = logx.WithSlog(ctx, logx.Redis) + log := logx.GetSlog(ctx) + var standaloneActual = &redisv1beta2.Redis{} + var sentinelActual = &redissentinelv1beta2.RedisSentinel{} + var replicationActual = &redisreplicationv1beta2.RedisReplication{} + + nsnBuilder := createNsNameBuilder(specNamespacedName) + + found, err := ctrlcommon.GetResource( + ctx, client, nsnBuilder.StandaloneNsName(), StandaloneType, standaloneActual, + ) + if err != nil { + return []metav1.Condition{ + { + Type: RedisStandaloneCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + }, nil + } + if !found { + standaloneActual = nil + } + + found, err = ctrlcommon.GetResource( + ctx, client, nsnBuilder.SentinelNsName(), SentinelType, sentinelActual, + ) + if err != nil { + return []metav1.Condition{ + { + Type: RedisSentinelCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + }, nil + } + if !found { + sentinelActual = nil + } + + found, err = ctrlcommon.GetResource( + ctx, client, nsnBuilder.ReplicationNsName(), ReplicationType, replicationActual, + ) + if err != nil { + return []metav1.Condition{ + { + Type: RedisReplicationCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + }, nil + } + if !found { + replicationActual = nil + } + + conditions := make([]metav1.Condition, 0) + var connection *apiv2.RedisConnection + + if standaloneActual == nil && replicationActual == nil && onDeleteRule.Policy == ctrlcommon.Purge { + log.Debug( + "Attempting to purge associated redis resources after deletion", + "specName", specNamespacedName.Name, + ) + if err := purgeAssociatedResources(ctx, client, specNamespacedName.Namespace, onDeleteRule.Selector); err != nil { + conditions = append(conditions, metav1.Condition{ + Type: RedisStandaloneCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }) + } else { + conditions = append(conditions, metav1.Condition{ + Type: RedisStandaloneCustomResourceType, + Status: metav1.ConditionFalse, + Reason: ctrlcommon.PendingDeleteReason, + }) + } + } + + if standaloneActual != nil { + connInfo := readStandaloneConnectionDetails(standaloneActual) + + connection, err = writeRedisConnInfo( + ctx, client, wandbOwner, nsnBuilder, connInfo, + ) + if err != nil { + return []metav1.Condition{ + { + Type: RedisConnectionInfoType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + }, nil + } + if connection == nil { + conditions = append(conditions, metav1.Condition{ + Type: RedisConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: ctrlcommon.NoResourceReason, + }) + } else { + conditions = append(conditions, metav1.Condition{ + Type: RedisConnectionInfoType, + Status: metav1.ConditionTrue, + Reason: ctrlcommon.ResourceExistsReason, + }) + } + + standalonePodsRunning, err := standalonePodsRunningStatus(ctx, client, standaloneActual) + if err != nil { + return []metav1.Condition{ + { + Type: RedisReportedReadyType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + }, nil + } + conditions = append(conditions, computeStandaloneReportedReadyCondition(ctx, standalonePodsRunning)...) + + } else if sentinelActual != nil && replicationActual != nil { + connInfo := readSentinelConnectionDetails(sentinelActual) + + connection, err = writeRedisConnInfo( + ctx, client, wandbOwner, nsnBuilder, connInfo, + ) + if err != nil { + return []metav1.Condition{ + { + Type: RedisConnectionInfoType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + }, nil + } + if connection == nil { + conditions = append(conditions, metav1.Condition{ + Type: RedisConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: ctrlcommon.NoResourceReason, + }) + } else { + conditions = append(conditions, metav1.Condition{ + Type: RedisConnectionInfoType, + Status: metav1.ConditionTrue, + Reason: ctrlcommon.ResourceExistsReason, + }) + } + + sentinelPodsRunning, err := sentinelPodsRunningStatus(ctx, client, sentinelActual) + if err != nil { + return []metav1.Condition{ + { + Type: RedisReportedReadyType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + }, nil + } + replicationPodsRunning, err := replicationPodsRunningStatus(ctx, client, replicationActual) + if err != nil { + return []metav1.Condition{ + { + Type: RedisReportedReadyType, + Status: metav1.ConditionUnknown, + Reason: ctrlcommon.ApiErrorReason, + }, + }, nil + } + conditions = append(conditions, computeSentinelReportedReadyCondition(ctx, sentinelPodsRunning, replicationPodsRunning)...) + } + + return conditions, connection +} + +func computeStandaloneReportedReadyCondition( + ctx context.Context, podsRunning map[string]bool, +) []metav1.Condition { + log := logx.GetSlog(ctx) + var runningCount, podCount int + + for _, isRunning := range podsRunning { + podCount++ + if isRunning { + runningCount++ + } + } + log.Info("Redis Standalone pods status", "running", runningCount, "total", podCount) + + status := metav1.ConditionUnknown + reason := ctrlcommon.UnknownReason + message := "" + + if podCount > 0 && podCount == runningCount { + status = metav1.ConditionTrue + reason = ctrlcommon.ResourceExistsReason + } else if podCount > 0 { + status = metav1.ConditionFalse + reason = ctrlcommon.ResourceExistsReason + log.Info("Redis Standalone pods not all running", "running", runningCount, "total", podCount) + } + + return []metav1.Condition{ + { + Type: RedisReportedReadyType, + Status: status, + Reason: reason, + Message: message, + }, + } +} + +func computeSentinelReportedReadyCondition( + ctx context.Context, sentinelPodsRunning, replicationPodsRunning map[string]bool, +) []metav1.Condition { + log := logx.GetSlog(ctx) + + var sentinelRunningCount, sentinelPodCount int + for _, isRunning := range sentinelPodsRunning { + sentinelPodCount++ + if isRunning { + sentinelRunningCount++ + } + } + log.Info("Redis Sentinel pods status", "running", sentinelRunningCount, "total", sentinelPodCount) + + var replicationRunningCount, replicationPodCount int + for _, isRunning := range replicationPodsRunning { + replicationPodCount++ + if isRunning { + replicationRunningCount++ + } + } + log.Info("Redis Replication pods status", "running", replicationRunningCount, "total", replicationPodCount) + + status := metav1.ConditionUnknown + reason := ctrlcommon.UnknownReason + message := "" + + allPodsRunning := sentinelPodCount > 0 && sentinelPodCount == sentinelRunningCount && + replicationPodCount > 0 && replicationPodCount == replicationRunningCount + atLeastOneEach := sentinelRunningCount > 0 && replicationRunningCount > 0 + eitherZero := sentinelRunningCount == 0 || replicationRunningCount == 0 + + if allPodsRunning { + status = metav1.ConditionTrue + reason = ctrlcommon.ResourceExistsReason + } else if eitherZero { + status = metav1.ConditionFalse + reason = ctrlcommon.ResourceExistsReason + message = fmt.Sprintf("sentinel: %d/%d running, replication: %d/%d running", sentinelRunningCount, sentinelPodCount, replicationRunningCount, replicationPodCount) + } else if atLeastOneEach { + status = metav1.ConditionFalse + reason = "degraded" + message = fmt.Sprintf("sentinel: %d/%d running, replication: %d/%d running", sentinelRunningCount, sentinelPodCount, replicationRunningCount, replicationPodCount) + } + + return []metav1.Condition{ + { + Type: RedisReportedReadyType, + Status: status, + Reason: reason, + Message: message, + }, + } +} + +func sentinelPodsRunningStatus( + ctx context.Context, client client.Client, sentinel *redissentinelv1beta2.RedisSentinel, +) ( + map[string]bool, error, +) { + var result = make(map[string]bool) + var found bool + var err error + + if sentinel == nil { + return result, nil + } + + podCount := sentinel.Spec.Size + if podCount == nil || *podCount == 0 { + return result, nil + } + + namespace := sentinel.Namespace + specName := sentinel.Name + + for i := 0; i < int(*podCount); i++ { + podName := fmt.Sprintf("%s-sentinel-%d", specName, i) + var pod = &corev1.Pod{} + nsName := types.NamespacedName{Namespace: namespace, Name: podName} + if found, err = ctrlcommon.GetResource( + ctx, client, nsName, "RedisSentinelPod", pod, + ); err != nil { + return result, err + } + if found { + result[podName] = pod.Status.Phase == corev1.PodRunning + } else { + result[podName] = false + } + } + return result, nil +} + +func replicationPodsRunningStatus( + ctx context.Context, client client.Client, replication *redisreplicationv1beta2.RedisReplication, +) ( + map[string]bool, error, +) { + var result = make(map[string]bool) + var found bool + var err error + + if replication == nil { + return result, nil + } + + podCount := replication.Spec.Size + if podCount == nil || *podCount == 0 { + return result, nil + } + + namespace := replication.Namespace + specName := replication.Name + + for i := 0; i < int(*podCount); i++ { + podName := fmt.Sprintf("%s-%d", specName, i) + var pod = &corev1.Pod{} + nsName := types.NamespacedName{Namespace: namespace, Name: podName} + if found, err = ctrlcommon.GetResource( + ctx, client, nsName, "RedisReplicaPod", pod, + ); err != nil { + return result, err + } + if found { + result[podName] = pod.Status.Phase == corev1.PodRunning + } else { + result[podName] = false + } + } + return result, nil +} + +func standalonePodsRunningStatus( + ctx context.Context, client client.Client, standalone *redisv1beta2.Redis, +) ( + map[string]bool, error, +) { + var result = make(map[string]bool) + var found bool + var err error + + if standalone == nil { + return result, nil + } + + podCount := 1 + + namespace := standalone.Namespace + specName := standalone.Name + + for i := 0; i < podCount; i++ { + podName := fmt.Sprintf("%s-%d", specName, i) + var pod = &corev1.Pod{} + nsName := types.NamespacedName{Namespace: namespace, Name: podName} + if found, err = ctrlcommon.GetResource( + ctx, client, nsName, "RedisPod", pod, + ); err != nil { + return result, err + } + if found { + result[podName] = pod.Status.Phase == corev1.PodRunning + } else { + result[podName] = false + } + } + return result, nil +} diff --git a/internal/controller/infra/managed/redis/opstree/spec.go b/internal/controller/infra/managed/redis/opstree/spec.go new file mode 100644 index 00000000..606ac2cd --- /dev/null +++ b/internal/controller/infra/managed/redis/opstree/spec.go @@ -0,0 +1,408 @@ +package opstree + +import ( + "context" + "fmt" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + "github.com/wandb/operator/pkg/utils" + rediscommon "github.com/wandb/operator/pkg/vendored/redis-operator/common/v1beta2" + redisv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redis/v1beta2" + redisreplicationv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redisreplication/v1beta2" + redissentinelv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redissentinel/v1beta2" + "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" +) + +const ( + RedisModuleName = "redis" +) + +const ( + DefaultSentinelGroup = "gorilla" + DefaultRedisExporterPort = 9121 + + defaultRedisStandaloneImage = "quay.io/opstree/redis:v7.0.15" + defaultRedisReplicationImage = "quay.io/opstree/redis:v7.0.15" + defaultRedisSentinelImage = "quay.io/opstree/redis-sentinel:v7.0.12" + defaultRedisExporterImage = "quay.io/opstree/redis-exporter:v1.44.0" +) + +func RedisStandaloneImage(img manifest.ImageRef, globalImageRegistry string) string { + if out := img.GetImage(globalImageRegistry); out != "" { + return out + } + return defaultRedisStandaloneImage +} + +func RedisReplicationImage(img manifest.ImageRef, globalImageRegistry string) string { + if out := img.GetImage(globalImageRegistry); out != "" { + return out + } + return defaultRedisReplicationImage +} + +func RedisSentinelImage(img manifest.ImageRef, globalImageRegistry string) string { + if out := img.GetImage(globalImageRegistry); out != "" { + return out + } + return defaultRedisSentinelImage +} + +func DefaultRedisExporterImage(img manifest.ImageRef, globalImageRegistry string) string { + if out := img.GetImage(globalImageRegistry); out != "" { + return out + } + return defaultRedisExporterImage +} + +const ( + redisRunAsUser int64 = 1000 + redisRunAsGroup int64 = 1000 + redisFSGroup int64 = 1000 + + redisWritableTmpVolumeName = "redis-tmp" + redisWritableTmpMountPath = "/tmp" + + redisCapabilityAll corev1.Capability = "ALL" +) + +func redisPodSecurityContext() *corev1.PodSecurityContext { + if utils.IsOpenShift() { + return &corev1.PodSecurityContext{ + RunAsNonRoot: ptr.To(true), + SeccompProfile: redisRuntimeDefaultSeccompProfile(), + } + } + + return &corev1.PodSecurityContext{ + RunAsUser: ptr.To(redisRunAsUser), + RunAsGroup: ptr.To(redisRunAsGroup), + RunAsNonRoot: ptr.To(true), + FSGroup: ptr.To(redisFSGroup), + SeccompProfile: redisRuntimeDefaultSeccompProfile(), + } +} + +func redisContainerSecurityContext() *corev1.SecurityContext { + securityContext := &corev1.SecurityContext{ + RunAsNonRoot: ptr.To(true), + AllowPrivilegeEscalation: ptr.To(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{redisCapabilityAll}, + }, + SeccompProfile: redisRuntimeDefaultSeccompProfile(), + } + if !utils.IsOpenShift() { + securityContext.RunAsUser = ptr.To(redisRunAsUser) + securityContext.RunAsGroup = ptr.To(redisRunAsGroup) + } + return securityContext +} + +func redisWritableVolumeMount() rediscommon.AdditionalVolume { + return rediscommon.AdditionalVolume{ + Volume: []corev1.Volume{ + { + Name: redisWritableTmpVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + MountPath: []corev1.VolumeMount{ + {Name: redisWritableTmpVolumeName, MountPath: redisWritableTmpMountPath}, + }, + } +} + +func redisAdditionalVolumePtr() *rediscommon.AdditionalVolume { + volumeMount := redisWritableVolumeMount() + return &volumeMount +} + +func redisRuntimeDefaultSeccompProfile() *corev1.SeccompProfile { + return &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault} +} + +// createRedisExporterConfig creates a RedisExporter configuration if telemetry is enabled. +// Returns nil if telemetry is disabled. +func createRedisExporterConfig(telemetry apiv2.Telemetry, img manifest.ImageRef, wandb *apiv2.WeightsAndBiases) *rediscommon.RedisExporter { + if !telemetry.Enabled { + return nil + } + + port := DefaultRedisExporterPort + return &rediscommon.RedisExporter{ + Enabled: true, + Port: &port, + Image: DefaultRedisExporterImage(img, wandb.Spec.Global.ImageRegistry), + ImagePullPolicy: corev1.PullIfNotPresent, + SecurityContext: redisContainerSecurityContext(), + } +} + +// ToRedisStandaloneVendorSpec converts a RedisSpec to a Redis standalone CR. +// This function creates a standalone Redis instance (no HA, no sentinel). +// Returns an error if sentinel is enabled in the spec. +func ToRedisStandaloneVendorSpec( + ctx context.Context, + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedRedisSpec, + scheme *runtime.Scheme, + mfst manifest.Manifest, +) (*redisv1beta2.Redis, error) { + _, log := logx.WithSlog(ctx, logx.Redis) + if spec == nil { + return nil, nil + } + + if spec.Sentinel.Enabled { + return nil, nil + } + + nsnBuilder := CreateNsNameBuilder(types.NamespacedName{ + Namespace: spec.Namespace, Name: spec.Name, + }) + + storageQuantity, err := resource.ParseQuantity(spec.StorageSize) + if err != nil { + return nil, fmt.Errorf("invalid storage size %q: %w", spec.StorageSize, err) + } + + redis := &redisv1beta2.Redis{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsnBuilder.StandaloneName(), + Namespace: nsnBuilder.StandaloneNamespace(), + }, + Spec: redisv1beta2.RedisSpec{ + KubernetesConfig: rediscommon.KubernetesConfig{ + Image: RedisStandaloneImage(mfst.Redis["default"].Images["standalone"], wandb.Spec.Global.ImageRegistry), + ImagePullPolicy: corev1.PullIfNotPresent, + Resources: &corev1.ResourceRequirements{}, + }, + Affinity: wandb.GetAffinity(spec.ManagedInfraSpec), + PodSecurityContext: redisPodSecurityContext(), + SecurityContext: redisContainerSecurityContext(), + Tolerations: wandb.GetTolerations(spec.ManagedInfraSpec), + Storage: &rediscommon.Storage{ + VolumeClaimTemplate: corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Labels: BuildWandbRedisLabels(wandb), + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: storageQuantity, + }, + }, + }, + }, + VolumeMount: redisWritableVolumeMount(), + }, + }, + } + + if len(spec.Config.Resources.Requests) > 0 || len(spec.Config.Resources.Limits) > 0 { + redis.Spec.KubernetesConfig.Resources = &corev1.ResourceRequirements{ + Requests: spec.Config.Resources.Requests, + Limits: spec.Config.Resources.Limits, + } + } + + if err := ctrl.SetControllerReference(wandb, redis, scheme); err != nil { + log.Error("failed to set owner reference on Redis CR", logx.ErrAttr(err)) + return nil, fmt.Errorf("failed to set owner reference: %w", err) + } + + redis.Spec.RedisExporter = createRedisExporterConfig(spec.Telemetry, mfst.Redis["default"].Images["exporter"], wandb) + + return redis, nil +} + +// ToRedisSentinelVendorSpec converts a RedisSpec to a RedisSentinel CR. +// This function creates a Redis Sentinel for HA configuration. +// Returns an error if sentinel is not enabled in the spec. +func ToRedisSentinelVendorSpec( + ctx context.Context, + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedRedisSpec, + scheme *runtime.Scheme, + mfst manifest.Manifest, +) (*redissentinelv1beta2.RedisSentinel, error) { + _, log := logx.WithSlog(ctx, logx.Redis) + if spec == nil { + return nil, nil + } + + if !spec.Sentinel.Enabled { + return nil, nil + } + + nsnBuilder := CreateNsNameBuilder(types.NamespacedName{ + Namespace: spec.Namespace, Name: spec.Name, + }) + + // TODO I dont think we want to default this at all? + sentinelCount := int32(3) + + // Get master name from config or use default + masterName := DefaultSentinelGroup + if spec.Sentinel.Config.MasterName != "" { + masterName = spec.Sentinel.Config.MasterName + } + + sentinel := &redissentinelv1beta2.RedisSentinel{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsnBuilder.SentinelName(), + Namespace: nsnBuilder.Namespace(), + }, + Spec: redissentinelv1beta2.RedisSentinelSpec{ + Size: &sentinelCount, + KubernetesConfig: rediscommon.KubernetesConfig{ + Image: RedisSentinelImage(mfst.Redis["default"].Images["sentinel"], wandb.Spec.Global.ImageRegistry), + ImagePullPolicy: corev1.PullIfNotPresent, + Resources: &corev1.ResourceRequirements{}, + }, + PodSecurityContext: redisPodSecurityContext(), + SecurityContext: redisContainerSecurityContext(), + Affinity: wandb.GetAffinity(spec.ManagedInfraSpec), + Tolerations: wandb.GetTolerations(spec.ManagedInfraSpec), + VolumeMount: redisAdditionalVolumePtr(), + RedisSentinelConfig: &redissentinelv1beta2.RedisSentinelConfig{ + RedisSentinelConfig: rediscommon.RedisSentinelConfig{ + RedisReplicationName: nsnBuilder.ReplicationName(), + MasterGroupName: masterName, + }, + }, + }, + } + + // Add resources if specified + if len(spec.Sentinel.Config.Resources.Requests) > 0 || len(spec.Sentinel.Config.Resources.Limits) > 0 { + sentinel.Spec.KubernetesConfig.Resources = &corev1.ResourceRequirements{ + Requests: spec.Sentinel.Config.Resources.Requests, + Limits: spec.Sentinel.Config.Resources.Limits, + } + } + + // Set owner reference + if err := ctrl.SetControllerReference(wandb, sentinel, scheme); err != nil { + log.Error("failed to set owner reference on RedisSentinel CR", logx.ErrAttr(err)) + return nil, fmt.Errorf("failed to set owner reference: %w", err) + } + + // Add RedisExporter if telemetry is enabled + sentinel.Spec.RedisExporter = createRedisExporterConfig(spec.Telemetry, mfst.Redis["default"].Images["exporter"], wandb) + + return sentinel, nil +} + +// ToRedisReplicationVendorSpec converts a RedisSpec to a RedisReplication CR. +// This function creates a Redis replication setup for HA configuration. +// Returns an error if sentinel is not enabled in the spec. +func ToRedisReplicationVendorSpec( + ctx context.Context, + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedRedisSpec, + scheme *runtime.Scheme, + mfst manifest.Manifest, +) (*redisreplicationv1beta2.RedisReplication, error) { + _, log := logx.WithSlog(ctx, logx.Redis) + if spec == nil { + return nil, nil + } + + if !spec.Sentinel.Enabled { + return nil, nil + } + + nsnBuilder := CreateNsNameBuilder(types.NamespacedName{ + Namespace: spec.Namespace, Name: spec.Name, + }) + + // Parse storage quantity + storageQuantity, err := resource.ParseQuantity(spec.StorageSize) + if err != nil { + log.Error("Failed to parse storage size", "storageSize", spec.StorageSize, "error", err) + return nil, fmt.Errorf("invalid storage size %q: %w", spec.StorageSize, err) + } + + // TODO I dont think we want to default this at all? + replicaCount := int32(3) + + replication := &redisreplicationv1beta2.RedisReplication{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsnBuilder.ReplicationName(), + Namespace: nsnBuilder.Namespace(), + }, + Spec: redisreplicationv1beta2.RedisReplicationSpec{ + Size: &replicaCount, + KubernetesConfig: rediscommon.KubernetesConfig{ + Image: RedisReplicationImage(mfst.Redis["default"].Images["replication"], wandb.Spec.Global.ImageRegistry), + ImagePullPolicy: corev1.PullIfNotPresent, + Resources: &corev1.ResourceRequirements{}, + }, + PodSecurityContext: redisPodSecurityContext(), + SecurityContext: redisContainerSecurityContext(), + Affinity: wandb.GetAffinity(spec.ManagedInfraSpec), + Tolerations: wandb.GetTolerations(spec.ManagedInfraSpec), + Storage: &rediscommon.Storage{ + VolumeClaimTemplate: corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Labels: BuildWandbRedisLabels(wandb), + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: storageQuantity, + }, + }, + }, + }, + VolumeMount: redisWritableVolumeMount(), + }, + }, + } + + // Add resources if specified + if len(spec.Config.Resources.Requests) > 0 || len(spec.Config.Resources.Limits) > 0 { + replication.Spec.KubernetesConfig.Resources = &corev1.ResourceRequirements{ + Requests: spec.Config.Resources.Requests, + Limits: spec.Config.Resources.Limits, + } + } + + // Set owner reference + if err := ctrl.SetControllerReference(wandb, replication, scheme); err != nil { + log.Error("failed to set owner reference on RedisReplication CR", logx.ErrAttr(err)) + return nil, fmt.Errorf("failed to set owner reference: %w", err) + } + + // Add RedisExporter if telemetry is enabled + replication.Spec.RedisExporter = createRedisExporterConfig(spec.Telemetry, mfst.Redis["default"].Images["exporter"], wandb) + + return replication, nil +} + +func BuildWandbRedisLabels(wandb *apiv2.WeightsAndBiases) map[string]string { + return common.BuildWandbLabels(wandb, RedisModuleName) +} + +func ToRedisOnDeleteRule(wandb *apiv2.WeightsAndBiases, retentionPolicy apiv2.RetentionPolicy) common.OnDeleteRule { + return common.ToOnDeleteRule(wandb, retentionPolicy, RedisModuleName) +} diff --git a/internal/controller/infra/managed/redis/opstree/spec_test.go b/internal/controller/infra/managed/redis/opstree/spec_test.go new file mode 100644 index 00000000..766b8b8f --- /dev/null +++ b/internal/controller/infra/managed/redis/opstree/spec_test.go @@ -0,0 +1,170 @@ +package opstree + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/pkg/wandb/manifest" + "github.com/wandb/operator/pkg/utils" + redisv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redis/v1beta2" + redisreplicationv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redisreplication/v1beta2" + redissentinelv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redissentinel/v1beta2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ = Describe("Redis vendor specs", func() { + BeforeEach(func() { + utils.SetOpenShiftMode(false) + }) + + It("renders hardened standalone Redis settings", func() { + wandb := redisWandb(false) + + redis, err := ToRedisStandaloneVendorSpec(context.Background(), wandb, wandb.Spec.Redis[apiv2.DefaultInstanceName].ManagedRedis, redisScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(redis).NotTo(BeNil()) + + expectRedisDefaultPodSecurityContext(redis.Spec.PodSecurityContext) + expectRedisDefaultContainerSecurityContext(redis.Spec.SecurityContext) + expectRedisWritableTmpMount(redis.Spec.Storage.VolumeMount.MountPath) + Expect(redis.Spec.RedisExporter).NotTo(BeNil()) + expectRedisDefaultContainerSecurityContext(redis.Spec.RedisExporter.SecurityContext) + }) + + It("renders hardened sentinel and replication Redis settings", func() { + wandb := redisWandb(true) + + sentinel, err := ToRedisSentinelVendorSpec(context.Background(), wandb, wandb.Spec.Redis[apiv2.DefaultInstanceName].ManagedRedis, redisScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(sentinel).NotTo(BeNil()) + expectRedisDefaultPodSecurityContext(sentinel.Spec.PodSecurityContext) + expectRedisDefaultContainerSecurityContext(sentinel.Spec.SecurityContext) + Expect(sentinel.Spec.VolumeMount).NotTo(BeNil()) + expectRedisWritableTmpMount(sentinel.Spec.VolumeMount.MountPath) + + replication, err := ToRedisReplicationVendorSpec(context.Background(), wandb, wandb.Spec.Redis[apiv2.DefaultInstanceName].ManagedRedis, redisScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(replication).NotTo(BeNil()) + expectRedisDefaultPodSecurityContext(replication.Spec.PodSecurityContext) + expectRedisDefaultContainerSecurityContext(replication.Spec.SecurityContext) + expectRedisWritableTmpMount(replication.Spec.Storage.VolumeMount.MountPath) + }) + + It("omits fixed Redis IDs in OpenShift mode", func() { + utils.SetOpenShiftMode(true) + + wandb := redisWandb(false) + redis, err := ToRedisStandaloneVendorSpec(context.Background(), wandb, wandb.Spec.Redis[apiv2.DefaultInstanceName].ManagedRedis, redisScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(redis).NotTo(BeNil()) + + expectOpenShiftPodSecurityContext(redis.Spec.PodSecurityContext) + expectOpenShiftContainerSecurityContext(redis.Spec.SecurityContext) + expectRedisWritableTmpMount(redis.Spec.Storage.VolumeMount.MountPath) + }) +}) + +func redisScheme() *runtime.Scheme { + scheme := runtime.NewScheme() + Expect(apiv2.AddToScheme(scheme)).To(Succeed()) + Expect(redisv1beta2.AddToScheme(scheme)).To(Succeed()) + Expect(redissentinelv1beta2.AddToScheme(scheme)).To(Succeed()) + Expect(redisreplicationv1beta2.AddToScheme(scheme)).To(Succeed()) + return scheme +} + +func redisWandb(sentinel bool) *apiv2.WeightsAndBiases { + return &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiv2.GroupVersion.String(), + Kind: "WeightsAndBiases", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "wandb", + }, + Spec: apiv2.WeightsAndBiasesSpec{ + Redis: map[string]apiv2.RedisSpec{ + apiv2.DefaultInstanceName: { + ManagedRedis: &apiv2.ManagedRedisSpec{ + Name: "redis", + Namespace: "wandb", + StorageSize: "1Gi", + Telemetry: apiv2.Telemetry{Enabled: true}, + Sentinel: apiv2.RedisSentinelSpec{Enabled: sentinel}, + }, + }, + }, + }, + } +} + +func expectRedisDefaultPodSecurityContext(securityContext *corev1.PodSecurityContext) { + Expect(securityContext).NotTo(BeNil()) + Expect(securityContext.RunAsUser).NotTo(BeNil()) + Expect(*securityContext.RunAsUser).To(Equal(redisRunAsUser)) + Expect(securityContext.RunAsGroup).NotTo(BeNil()) + Expect(*securityContext.RunAsGroup).To(Equal(redisRunAsGroup)) + Expect(securityContext.RunAsNonRoot).NotTo(BeNil()) + Expect(*securityContext.RunAsNonRoot).To(BeTrue()) + Expect(securityContext.FSGroup).NotTo(BeNil()) + Expect(*securityContext.FSGroup).To(Equal(redisFSGroup)) + Expect(securityContext.SeccompProfile).NotTo(BeNil()) + Expect(securityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) +} + +func expectRedisDefaultContainerSecurityContext(securityContext *corev1.SecurityContext) { + Expect(securityContext).NotTo(BeNil()) + Expect(securityContext.RunAsUser).NotTo(BeNil()) + Expect(*securityContext.RunAsUser).To(Equal(redisRunAsUser)) + Expect(securityContext.RunAsGroup).NotTo(BeNil()) + Expect(*securityContext.RunAsGroup).To(Equal(redisRunAsGroup)) + Expect(securityContext.RunAsNonRoot).NotTo(BeNil()) + Expect(*securityContext.RunAsNonRoot).To(BeTrue()) + Expect(securityContext.AllowPrivilegeEscalation).NotTo(BeNil()) + Expect(*securityContext.AllowPrivilegeEscalation).To(BeFalse()) + Expect(securityContext.Capabilities).NotTo(BeNil()) + Expect(securityContext.Capabilities.Drop).To(ContainElement(redisCapabilityAll)) + Expect(securityContext.SeccompProfile).NotTo(BeNil()) + Expect(securityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) +} + +func expectRedisWritableTmpMount(mounts []corev1.VolumeMount) { + found := false + for _, mount := range mounts { + if mount.Name == redisWritableTmpVolumeName && mount.MountPath == redisWritableTmpMountPath { + found = true + break + } + } + Expect(found).To(BeTrue()) +} + +func expectOpenShiftPodSecurityContext(securityContext *corev1.PodSecurityContext) { + Expect(securityContext).NotTo(BeNil()) + Expect(securityContext.RunAsUser).To(BeNil()) + Expect(securityContext.RunAsGroup).To(BeNil()) + Expect(securityContext.FSGroup).To(BeNil()) + Expect(securityContext.RunAsNonRoot).NotTo(BeNil()) + Expect(*securityContext.RunAsNonRoot).To(BeTrue()) + Expect(securityContext.SeccompProfile).NotTo(BeNil()) + Expect(securityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) +} + +func expectOpenShiftContainerSecurityContext(securityContext *corev1.SecurityContext) { + Expect(securityContext).NotTo(BeNil()) + Expect(securityContext.RunAsUser).To(BeNil()) + Expect(securityContext.RunAsGroup).To(BeNil()) + Expect(securityContext.RunAsNonRoot).NotTo(BeNil()) + Expect(*securityContext.RunAsNonRoot).To(BeTrue()) + Expect(securityContext.AllowPrivilegeEscalation).NotTo(BeNil()) + Expect(*securityContext.AllowPrivilegeEscalation).To(BeFalse()) + Expect(securityContext.Capabilities).NotTo(BeNil()) + Expect(securityContext.Capabilities.Drop).To(ContainElement(redisCapabilityAll)) + Expect(securityContext.SeccompProfile).NotTo(BeNil()) + Expect(securityContext.SeccompProfile.Type).To(Equal(corev1.SeccompProfileTypeRuntimeDefault)) +} diff --git a/internal/controller/infra/managed/redis/opstree/status.go b/internal/controller/infra/managed/redis/opstree/status.go new file mode 100644 index 00000000..930674c0 --- /dev/null +++ b/internal/controller/infra/managed/redis/opstree/status.go @@ -0,0 +1,260 @@ +package opstree + +import ( + "context" + "time" + + "github.com/samber/lo" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" +) + +const ( + RedisStandaloneCustomResourceType = "RedisStandaloneCustomResource" + RedisSentinelCustomResourceType = "RedisSentinelCustomResource" + RedisReplicationCustomResourceType = "RedisReplicationCustomResource" + RedisConnectionInfoType = "RedisConnectionInfo" + RedisReportedReadyType = "RedisReportedReady" +) + +func ComputeStatus( + ctx context.Context, + enabled bool, + oldConditions, currentConditions []metav1.Condition, + connection *apiv2.RedisConnection, + currentGeneration int64, +) (apiv2.RedisInfraStatus, []corev1.Event, ctrl.Result) { + ctx, _ = logx.WithSlog(ctx, logx.Redis) + result := apiv2.RedisInfraStatus{} + + if connection != nil { + result.Connection = *connection + } + + currentConditions = applyDefaultConditions(currentConditions) + + result.Conditions = common.ComputeConditionUpdates( + oldConditions, + currentConditions, + currentGeneration, + common.DefaultConditionExpiry, + ) + + state, events := inferInfraState(ctx, enabled, result.Conditions) + result.State = state + + result.Ready = !lo.Contains(common.NotReadyStates, result.State) + + requeueAfter := 3 * time.Minute + switch result.State { + case common.ErrorState: + requeueAfter = 15 * time.Second + case common.DegradedState: + requeueAfter = 5 * time.Minute + case common.PendingState: + requeueAfter = 2 * time.Minute + case common.HealthyState: + requeueAfter = 10 * time.Minute + } + + return result, events, ctrl.Result{RequeueAfter: requeueAfter} +} + +func applyDefaultConditions(conditions []metav1.Condition) []metav1.Condition { + if !common.ContainsType(conditions, RedisConnectionInfoType) { + conditions = append(conditions, metav1.Condition{ + Type: RedisConnectionInfoType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }) + } + + return conditions +} + +func inferInfraState( + ctx context.Context, + enabled bool, + conditions []metav1.Condition, +) (string, []corev1.Event) { + if !enabled { + return common.UnavailableState, nil + } + var events []corev1.Event + impliedStates := make(map[string]string, len(conditions)) + + impliedStates = inferStateFromCondition(ctx, RedisStandaloneCustomResourceType, impliedStates, conditions) + impliedStates = inferStateFromCondition(ctx, RedisSentinelCustomResourceType, impliedStates, conditions) + impliedStates = inferStateFromCondition(ctx, RedisReplicationCustomResourceType, impliedStates, conditions) + impliedStates = inferStateFromCondition(ctx, RedisConnectionInfoType, impliedStates, conditions) + impliedStates = inferStateFromCondition(ctx, RedisReportedReadyType, impliedStates, conditions) + + hasImpliedState := func(target string) bool { + return len(lo.FilterValues( + impliedStates, + func(_ string, value string) bool { + return value == target + })) > 0 + } + + summaryState := common.UnknownState + + // if the service is reporting as healthy but the connection info is unavailable + // log the missing connection as an event and mark the infraStatus as 'error' + if impliedStates[RedisConnectionInfoType] == common.UnavailableState && + impliedStates[RedisReportedReadyType] == common.HealthyState { + events = append(events, corev1.Event{ + Type: corev1.EventTypeWarning, + Reason: "RedisConnectionInfoUnavailable", + Message: "Redis connection info is unavailable, but Redis is reported as ready.", + }) + summaryState = common.ErrorState + } + + // if there is a specific state identified, use that + if summaryState != common.UnknownState { + return summaryState, events + } + + // otherwise, return the most significant state mapped for any condition + stateSignificanceOrder := []string{ + common.ErrorState, + common.UnavailableState, + common.PendingState, + common.DegradedState, + common.HealthyState, + } + for _, s := range stateSignificanceOrder { + if hasImpliedState(s) { + return s, events + } + } + + return common.UnknownState, events +} + +func inferStateFromCondition(ctx context.Context, conditionType string, impliedStates map[string]string, conditions []metav1.Condition) map[string]string { + cond, found := lo.Find(conditions, func(c metav1.Condition) bool { return c.Type == conditionType }) + if !found { + impliedStates[conditionType] = common.UnknownState + } else { + switch conditionType { + case RedisStandaloneCustomResourceType: + impliedStates[conditionType] = inferState_RedisStandaloneCustomResourceType(ctx, cond) + case RedisSentinelCustomResourceType: + impliedStates[conditionType] = inferState_RedisSentinelCustomResourceType(ctx, cond) + case RedisReplicationCustomResourceType: + impliedStates[conditionType] = inferState_RedisReplicationCustomResourceType(ctx, cond) + case RedisConnectionInfoType: + impliedStates[conditionType] = inferState_RedisConnectionInfoType(ctx, cond) + case RedisReportedReadyType: + impliedStates[conditionType] = inferState_RedisReportedReadyType(ctx, cond) + default: + impliedStates[conditionType] = common.UnknownState + } + } + return impliedStates +} + +func inferState_RedisStandaloneCustomResourceType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + if condition.Reason == common.PendingCreateReason { + result = common.PendingState + } + if condition.Reason == common.PendingDeleteReason { + result = common.UnavailableState + } + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} + +func inferState_RedisSentinelCustomResourceType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + if condition.Reason == common.PendingCreateReason { + result = common.PendingState + } + if condition.Reason == common.PendingDeleteReason { + result = common.UnavailableState + } + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} + +func inferState_RedisReplicationCustomResourceType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + if condition.Reason == common.PendingCreateReason { + result = common.PendingState + } + if condition.Reason == common.PendingDeleteReason { + result = common.UnavailableState + } + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} + +func inferState_RedisConnectionInfoType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + result = common.UnavailableState + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} + +func inferState_RedisReportedReadyType(ctx context.Context, condition metav1.Condition) string { + log := logx.GetSlog(ctx) + result := common.UnknownState + if condition.Status == metav1.ConditionTrue { + result = common.HealthyState + } + if condition.Status == metav1.ConditionFalse { + if condition.Reason == "degraded" { + result = common.DegradedState + } else { + result = common.UnavailableState + } + } + log.Debug( + "implied state", "state", result, "condition", condition.Type, + "reason", condition.Reason, "status", condition.Status, + ) + return result +} diff --git a/internal/controller/infra/managed/redis/opstree/values.go b/internal/controller/infra/managed/redis/opstree/values.go new file mode 100644 index 00000000..ee6b5998 --- /dev/null +++ b/internal/controller/infra/managed/redis/opstree/values.go @@ -0,0 +1,13 @@ +package opstree + +// installType represents the Redis installation type +type installType string + +const ( + // InstallTypeNone represents no Redis installation + InstallTypeNone installType = "none" + // InstallTypeSentinel represents Redis Sentinel installation + InstallTypeSentinel installType = "sentinel" + // InstallTypeStandalone represents Redis Standalone installation + InstallTypeStandalone installType = "standalone" +) diff --git a/internal/controller/infra/managed/redis/opstree/write.go b/internal/controller/infra/managed/redis/opstree/write.go new file mode 100644 index 00000000..a08c58e5 --- /dev/null +++ b/internal/controller/infra/managed/redis/opstree/write.go @@ -0,0 +1,386 @@ +package opstree + +import ( + "context" + "fmt" + "maps" + "strings" + + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + redisv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redis/v1beta2" + redisreplicationv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redisreplication/v1beta2" + redissentinelv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redissentinel/v1beta2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + StandaloneType = "RedisStandalone" + SentinelType = "RedisSentinel" + ReplicationType = "RedisReplication" + AppConnTypeName = "RedisAppConn" + + // pvcTemplatePrefix is the volumeClaimTemplate name the opstree operator + // uses when creating StatefulSets, resulting in PVCs named + // "redis-data-{crName}-{ordinal}". + pvcTemplatePrefix = "redis-data" +) + +func WriteState( + ctx context.Context, + cl client.Client, + specNamespacedName types.NamespacedName, + standaloneDesired *redisv1beta2.Redis, + sentinelDesired *redissentinelv1beta2.RedisSentinel, + replicationDesired *redisreplicationv1beta2.RedisReplication, + wandbLabels map[string]string, +) []metav1.Condition { + ctx, _ = logx.WithSlog(ctx, logx.Redis) + results := make([]metav1.Condition, 0) + + nsnBuilder := createNsNameBuilder(specNamespacedName) + + results = append(results, writeStandaloneState(ctx, cl, nsnBuilder, standaloneDesired)...) + results = append(results, writeSentinelState(ctx, cl, nsnBuilder, sentinelDesired)...) + results = append(results, writeReplicationState(ctx, cl, nsnBuilder, replicationDesired)...) + + if len(wandbLabels) > 0 { + var pvcPrefixes []string + var podPrefixes []string + if standaloneDesired != nil { + pvcPrefixes = append(pvcPrefixes, fmt.Sprintf("%s-%s-", pvcTemplatePrefix, nsnBuilder.StandaloneName())) + podPrefixes = append(podPrefixes, fmt.Sprintf("%s-", nsnBuilder.StandaloneName())) + } + if replicationDesired != nil { + pvcPrefixes = append(pvcPrefixes, fmt.Sprintf("%s-%s-", pvcTemplatePrefix, nsnBuilder.ReplicationName())) + podPrefixes = append(podPrefixes, fmt.Sprintf("%s-", nsnBuilder.ReplicationName())) + } + if err := ensurePVCLabels(ctx, cl, specNamespacedName.Namespace, pvcPrefixes, wandbLabels); err != nil { + results = append(results, metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }) + } + if err := ensurePodLabels(ctx, cl, specNamespacedName.Namespace, podPrefixes, wandbLabels); err != nil { + results = append(results, metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }) + } + } + + return results +} + +// ensurePVCLabels patches PVCs whose names match any of the given prefixes +// that are missing the wandb labels. The opstree operator creates PVCs via +// StatefulSet volumeClaimTemplates named "redis-data", so PVCs are named +// "redis-data-{crName}-{ordinal}". +func ensurePVCLabels( + ctx context.Context, + cl client.Client, + namespace string, + namePrefixes []string, + labels map[string]string, +) error { + log := logx.GetSlog(ctx) + + pvcList := &corev1.PersistentVolumeClaimList{} + if err := cl.List(ctx, pvcList, &client.ListOptions{Namespace: namespace}); err != nil { + return err + } + + for _, pvc := range pvcList.Items { + if !matchesAnyPrefix(pvc.Name, namePrefixes) { + continue + } + if common.HasAllLabelKeys(pvc.Labels, labels) { + continue + } + patch := client.MergeFrom(pvc.DeepCopy()) + if pvc.Labels == nil { + pvc.Labels = make(map[string]string) + } + maps.Copy(pvc.Labels, labels) + if err := cl.Patch(ctx, &pvc, patch); err != nil { + log.Error("failed to patch PVC labels", logx.ErrAttr(err), "pvc", pvc.Name) + return err + } + log.Debug("patched wandb labels onto PVC", "pvc", pvc.Name) + } + return nil +} + +func ensurePodLabels( + ctx context.Context, + cl client.Client, + namespace string, + namePrefixes []string, + labels map[string]string, +) error { + log := logx.GetSlog(ctx) + + podList := &corev1.PodList{} + if err := cl.List(ctx, podList, &client.ListOptions{Namespace: namespace}); err != nil { + return err + } + + for _, pod := range podList.Items { + if !matchesAnyPrefix(pod.Name, namePrefixes) { + continue + } + if common.HasAllLabelKeys(pod.Labels, labels) { + continue + } + patch := client.MergeFrom(pod.DeepCopy()) + if pod.Labels == nil { + pod.Labels = make(map[string]string) + } + maps.Copy(pod.Labels, labels) + if err := cl.Patch(ctx, &pod, patch); err != nil { + log.Error("failed to patch Pod labels", logx.ErrAttr(err), "pod", pod.Name) + return err + } + log.Debug("patched wandb labels onto Pod", "pod", pod.Name) + } + return nil +} + +func matchesAnyPrefix(name string, prefixes []string) bool { + for _, p := range prefixes { + if strings.HasPrefix(name, p) { + return true + } + } + return false +} + +func writeStandaloneState( + ctx context.Context, + cl client.Client, + nsnBuilder *NsNameBuilder, + standaloneDesired *redisv1beta2.Redis, +) []metav1.Condition { + var standaloneActual = &redisv1beta2.Redis{} + + found, err := common.GetResource( + ctx, cl, nsnBuilder.StandaloneNsName(), StandaloneType, standaloneActual, + ) + if err != nil { + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }, + { + Type: RedisStandaloneCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: common.ApiErrorReason, + }, + } + } + if !found { + standaloneActual = nil + } + + result := make([]metav1.Condition, 0) + + action, err := common.CrudResource(ctx, cl, standaloneDesired, standaloneActual) + if err != nil { + result = append(result, metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }) + } + + switch action { + case common.CreateAction: + result = append(result, metav1.Condition{ + Type: RedisStandaloneCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.PendingCreateReason, + }) + case common.DeleteAction: + result = append(result, metav1.Condition{ + Type: RedisStandaloneCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.PendingDeleteReason, + }) + case common.UpdateAction, common.UnchangedAction: + result = append(result, metav1.Condition{ + Type: RedisStandaloneCustomResourceType, + Status: metav1.ConditionTrue, + Reason: common.ResourceExistsReason, + }) + case common.NoAction: + result = append(result, metav1.Condition{ + Type: RedisStandaloneCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }) + } + + return result +} + +func writeSentinelState( + ctx context.Context, + cl client.Client, + nsnBuilder *NsNameBuilder, + sentinelDesired *redissentinelv1beta2.RedisSentinel, +) []metav1.Condition { + var sentinelActual = &redissentinelv1beta2.RedisSentinel{} + + found, err := common.GetResource( + ctx, cl, nsnBuilder.SentinelNsName(), SentinelType, sentinelActual, + ) + if err != nil { + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }, + { + Type: RedisSentinelCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: common.ApiErrorReason, + }, + } + } + if !found { + sentinelActual = nil + } + + result := make([]metav1.Condition, 0) + + action, err := common.CrudResource(ctx, cl, sentinelDesired, sentinelActual) + if err != nil { + result = append(result, metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }) + } + + switch action { + case common.CreateAction: + result = append(result, metav1.Condition{ + Type: RedisSentinelCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.PendingCreateReason, + }) + case common.DeleteAction: + result = append(result, metav1.Condition{ + Type: RedisSentinelCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.PendingDeleteReason, + }) + case common.UpdateAction, common.UnchangedAction: + result = append(result, metav1.Condition{ + Type: RedisSentinelCustomResourceType, + Status: metav1.ConditionTrue, + Reason: common.ResourceExistsReason, + }) + case common.NoAction: + result = append(result, metav1.Condition{ + Type: RedisSentinelCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }) + } + + return result +} + +func writeReplicationState( + ctx context.Context, + cl client.Client, + nsnBuilder *NsNameBuilder, + replicationDesired *redisreplicationv1beta2.RedisReplication, +) []metav1.Condition { + var replicationActual = &redisreplicationv1beta2.RedisReplication{} + + found, err := common.GetResource( + ctx, cl, nsnBuilder.ReplicationNsName(), ReplicationType, replicationActual, + ) + if err != nil { + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }, + { + Type: RedisReplicationCustomResourceType, + Status: metav1.ConditionUnknown, + Reason: common.ApiErrorReason, + }, + } + } + if !found { + replicationActual = nil + } + + result := make([]metav1.Condition, 0) + + action, err := common.CrudResource(ctx, cl, replicationDesired, replicationActual) + if err != nil { + result = append(result, metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }) + } + + switch action { + case common.CreateAction: + result = append(result, metav1.Condition{ + Type: RedisReplicationCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.PendingCreateReason, + }) + case common.DeleteAction: + result = append(result, metav1.Condition{ + Type: RedisReplicationCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.PendingDeleteReason, + }) + case common.UpdateAction, common.UnchangedAction: + result = append(result, metav1.Condition{ + Type: RedisReplicationCustomResourceType, + Status: metav1.ConditionTrue, + Reason: common.ResourceExistsReason, + }) + case common.NoAction: + result = append(result, metav1.Condition{ + Type: RedisReplicationCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.NoResourceReason, + }) + } + + return result +} + +type redisConnInfo struct { + Host string + Port string + SentinelHost string + SentinelPort string + SentinelMaster string +} + +func (c *redisConnInfo) toURL() string { + if c.SentinelHost != "" { + return fmt.Sprintf("redis://%s:%s?master=%s", c.SentinelHost, c.SentinelPort, c.SentinelMaster) + } + return fmt.Sprintf("redis://%s:%s", c.Host, c.Port) +} diff --git a/internal/controller/reconciler/cleanup_legacy_v1.go b/internal/controller/reconciler/cleanup_legacy_v1.go new file mode 100644 index 00000000..fa1b05ea --- /dev/null +++ b/internal/controller/reconciler/cleanup_legacy_v1.go @@ -0,0 +1,113 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "context" + "errors" + "fmt" + "sort" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/logx" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + appsv1 "k8s.io/api/apps/v1" + apiErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" +) + +// legacyV1DeploymentSuffixes names the Deployments the v1 Helm chart +// leaves behind after a CR has been converted to v2. The v2 reconciler +// creates its own Application-owned Deployments under different names, +// so these are orphans that must be deleted explicitly — they have no +// owner reference pointing at the v2 CR, so garbage collection won't +// catch them. +var legacyV1DeploymentSuffixes = []string{ + "app-bc", + "console-bc", + "executor-bc", + "parquet-bc", + "weave-bc", +} + +func buildDesiredAppNames(manifest serverManifest.Manifest) map[string]bool { + out := make(map[string]bool) + for _, app := range sortedManifestApplications(manifest) { + if len(app.Features) > 0 && !manifest.FeaturesEnabled(app.Features) { + continue + } + out[app.Name] = true + } + return out +} + +// deploymentsHealthy reports whether every manifest-desired application's +// Deployment is fully rolled out, plus the sorted names still blocking. It +// reads live Deployments rather than status.wandb.applications so the legacy +// cleanup gate cannot act on a stale status snapshot. +func deploymentsHealthy( + ctx context.Context, + c ctrlClient.Client, + namespace string, + desiredAppNames map[string]bool, +) (bool, []string) { + if len(desiredAppNames) == 0 { + return false, nil + } + var notReady []string + for name := range desiredAppNames { + dep := &appsv1.Deployment{} + if err := c.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, dep); err != nil { + notReady = append(notReady, name) + continue + } + if dep.Status.ObservedGeneration != dep.Generation || + dep.Status.ReadyReplicas != dep.Status.Replicas || + dep.Status.Replicas == 0 { + notReady = append(notReady, name) + } + } + sort.Strings(notReady) + return len(notReady) == 0, notReady +} + +func cleanupLegacyV1Deployments( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, +) error { + logger := logx.GetSlog(ctx) + var errs []error + for _, suffix := range legacyV1DeploymentSuffixes { + name := fmt.Sprintf("%s-%s", wandb.Name, suffix) + dep := &appsv1.Deployment{} + if err := c.Get(ctx, types.NamespacedName{Name: name, Namespace: wandb.Namespace}, dep); err != nil { + if apiErrors.IsNotFound(err) { + continue + } + errs = append(errs, fmt.Errorf("get legacy deployment %s: %w", name, err)) + continue + } + logger.Info("Deleting legacy v1 deployment", "deployment", name) + if err := c.Delete(ctx, dep, ctrlClient.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil && !apiErrors.IsNotFound(err) { + errs = append(errs, fmt.Errorf("delete legacy deployment %s: %w", name, err)) + } + } + return errors.Join(errs...) +} diff --git a/internal/controller/reconciler/cleanup_legacy_v1_test.go b/internal/controller/reconciler/cleanup_legacy_v1_test.go new file mode 100644 index 00000000..6e64a8b0 --- /dev/null +++ b/internal/controller/reconciler/cleanup_legacy_v1_test.go @@ -0,0 +1,287 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apiErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + + apiv2 "github.com/wandb/operator/api/v2" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" +) + +func newCleanupFixtureScheme(t *testing.T) *runtime.Scheme { + t.Helper() + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + require.NoError(t, appsv1.AddToScheme(scheme)) + require.NoError(t, apiv2.AddToScheme(scheme)) + return scheme +} + +func legacyDeployment(wandbName, suffix, namespace string) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%s", wandbName, suffix), + Namespace: namespace, + }, + } +} + +// readyDeployment builds a fully rolled-out Deployment for gate tests. +func readyDeployment(name, namespace string) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace, Generation: 2}, + Status: appsv1.DeploymentStatus{ + ObservedGeneration: 2, + Replicas: 1, + ReadyReplicas: 1, + }, + } +} + +func TestDeploymentsHealthy(t *testing.T) { + const namespace = "default" + desired := map[string]bool{"api": true, "console": true} + + tests := []struct { + name string + desired map[string]bool + deployments []*appsv1.Deployment + wantHealthy bool + wantBlocked []string + }{ + { + name: "empty desired set is never healthy", + desired: map[string]bool{}, + wantHealthy: false, + }, + { + name: "missing deployment blocks gate", + desired: desired, + deployments: []*appsv1.Deployment{readyDeployment("api", namespace)}, + wantHealthy: false, + wantBlocked: []string{"console"}, + }, + { + name: "mid-rollout deployment blocks gate", + desired: desired, + deployments: func() []*appsv1.Deployment { + rolling := readyDeployment("console", namespace) + rolling.Status.ReadyReplicas = 0 + return []*appsv1.Deployment{readyDeployment("api", namespace), rolling} + }(), + wantHealthy: false, + wantBlocked: []string{"console"}, + }, + { + name: "zero replicas blocks gate", + desired: desired, + deployments: func() []*appsv1.Deployment { + scaled := readyDeployment("console", namespace) + scaled.Status.Replicas = 0 + scaled.Status.ReadyReplicas = 0 + return []*appsv1.Deployment{readyDeployment("api", namespace), scaled} + }(), + wantHealthy: false, + wantBlocked: []string{"console"}, + }, + { + name: "stale observedGeneration blocks gate", + desired: desired, + deployments: func() []*appsv1.Deployment { + stale := readyDeployment("console", namespace) + stale.Status.ObservedGeneration = 1 + return []*appsv1.Deployment{readyDeployment("api", namespace), stale} + }(), + wantHealthy: false, + wantBlocked: []string{"console"}, + }, + { + name: "all deployments rolled out opens gate", + desired: desired, + deployments: []*appsv1.Deployment{readyDeployment("api", namespace), readyDeployment("console", namespace)}, + wantHealthy: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + builder := fake.NewClientBuilder().WithScheme(newCleanupFixtureScheme(t)) + for _, dep := range tc.deployments { + builder = builder.WithObjects(dep) + } + healthy, blocked := deploymentsHealthy(context.Background(), builder.Build(), namespace, tc.desired) + require.Equal(t, tc.wantHealthy, healthy) + require.Equal(t, tc.wantBlocked, blocked) + }) + } +} + +func TestBuildDesiredAppNames(t *testing.T) { + manifest := serverManifest.Manifest{ + Features: map[string]bool{ + "weaveTrace": true, + "disabled": false, + }, + Applications: map[string]serverManifest.Application{ + "api": {Name: "api"}, + "console": {Name: "console"}, + "weave-trace": {Name: "weave-trace", Features: []string{"weaveTrace"}}, + "behind-disabled": {Name: "behind-disabled", Features: []string{"disabled"}}, + "behind-unset-flag": {Name: "behind-unset-flag", Features: []string{"missing"}}, + }, + } + + got := buildDesiredAppNames(manifest) + require.Equal(t, map[string]bool{ + "api": true, + "console": true, + "weave-trace": true, + }, got) +} + +func TestCleanupLegacyV1Deployments(t *testing.T) { + const ( + wandbName = "wandb" + namespace = "default" + ) + + allSuffixes := legacyV1DeploymentSuffixes + + t.Run("no deployments seeded is a no-op", func(t *testing.T) { + scheme := newCleanupFixtureScheme(t) + c := fake.NewClientBuilder().WithScheme(scheme).Build() + wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: wandbName, Namespace: namespace}} + + require.NoError(t, cleanupLegacyV1Deployments(context.Background(), c, wandb)) + }) + + t.Run("all five deployments are deleted when present", func(t *testing.T) { + scheme := newCleanupFixtureScheme(t) + var seeds []ctrlClient.Object + for _, suffix := range allSuffixes { + seeds = append(seeds, legacyDeployment(wandbName, suffix, namespace)) + } + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(seeds...).Build() + wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: wandbName, Namespace: namespace}} + + require.NoError(t, cleanupLegacyV1Deployments(context.Background(), c, wandb)) + + for _, suffix := range allSuffixes { + err := c.Get(context.Background(), types.NamespacedName{ + Name: fmt.Sprintf("%s-%s", wandbName, suffix), + Namespace: namespace, + }, &appsv1.Deployment{}) + require.True(t, apiErrors.IsNotFound(err), "expected %s-%s deleted, got %v", wandbName, suffix, err) + } + }) + + t.Run("only present deployments are touched", func(t *testing.T) { + scheme := newCleanupFixtureScheme(t) + present := []string{"app-bc", "weave-bc"} + var seeds []ctrlClient.Object + for _, suffix := range present { + seeds = append(seeds, legacyDeployment(wandbName, suffix, namespace)) + } + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(seeds...).Build() + wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: wandbName, Namespace: namespace}} + + require.NoError(t, cleanupLegacyV1Deployments(context.Background(), c, wandb)) + + for _, suffix := range present { + err := c.Get(context.Background(), types.NamespacedName{ + Name: fmt.Sprintf("%s-%s", wandbName, suffix), + Namespace: namespace, + }, &appsv1.Deployment{}) + require.True(t, apiErrors.IsNotFound(err), "expected %s-%s deleted, got %v", wandbName, suffix, err) + } + }) + + t.Run("non-suffixed deployments in same namespace are untouched", func(t *testing.T) { + scheme := newCleanupFixtureScheme(t) + other := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: wandbName + "-app", Namespace: namespace}, + } + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(other).Build() + wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: wandbName, Namespace: namespace}} + + require.NoError(t, cleanupLegacyV1Deployments(context.Background(), c, wandb)) + + require.NoError(t, c.Get(context.Background(), types.NamespacedName{ + Name: wandbName + "-app", Namespace: namespace, + }, &appsv1.Deployment{})) + }) + + t.Run("delete failure on one suffix does not strand the rest", func(t *testing.T) { + scheme := newCleanupFixtureScheme(t) + var seeds []ctrlClient.Object + for _, suffix := range allSuffixes { + seeds = append(seeds, legacyDeployment(wandbName, suffix, namespace)) + } + failOn := fmt.Sprintf("%s-%s", wandbName, allSuffixes[0]) + + c := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(seeds...). + WithInterceptorFuncs(interceptor.Funcs{ + Delete: func(ctx context.Context, cl ctrlClient.WithWatch, obj ctrlClient.Object, opts ...ctrlClient.DeleteOption) error { + if obj.GetName() == failOn { + return fmt.Errorf("synthetic delete error") + } + return cl.Delete(ctx, obj, opts...) + }, + }). + Build() + wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: wandbName, Namespace: namespace}} + + err := cleanupLegacyV1Deployments(context.Background(), c, wandb) + require.Error(t, err) + require.Contains(t, err.Error(), "synthetic delete error") + + err = c.Get(context.Background(), types.NamespacedName{Name: failOn, Namespace: namespace}, &appsv1.Deployment{}) + require.NoError(t, err, "deployment that failed to delete should still exist") + + for _, suffix := range allSuffixes[1:] { + err := c.Get(context.Background(), types.NamespacedName{ + Name: fmt.Sprintf("%s-%s", wandbName, suffix), + Namespace: namespace, + }, &appsv1.Deployment{}) + require.True(t, apiErrors.IsNotFound(err), "expected %s-%s deleted, got %v", wandbName, suffix, err) + } + }) +} + +func TestDeploymentsHealthy_BlockedListSorted(t *testing.T) { + cl := fake.NewClientBuilder().WithScheme(newCleanupFixtureScheme(t)).Build() + healthy, blocked := deploymentsHealthy(context.Background(), cl, "default", + map[string]bool{"weave": true, "api": true, "glue": true}) + require.False(t, healthy) + require.Equal(t, []string{"api", "glue", "weave"}, blocked) +} diff --git a/internal/controller/reconciler/clickhouse.go b/internal/controller/reconciler/clickhouse.go new file mode 100644 index 00000000..bfdff2ea --- /dev/null +++ b/internal/controller/reconciler/clickhouse.go @@ -0,0 +1,310 @@ +package reconciler + +import ( + "context" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/external" + externalch "github.com/wandb/operator/internal/controller/infra/external/clickhouse" + "github.com/wandb/operator/internal/controller/infra/managed/clickhouse/altinity" + "github.com/wandb/operator/internal/controller/infra/managed/clickhouse/altinity/keeper" + "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/pkg/wandb/manifest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// clickHouseObjectStoreInstance is the object-store instance name managed +// ClickHouse prefers for its S3 disk; ResolveInstance falls back to the +// default instance when it is not provisioned. +const clickHouseObjectStoreInstance = "clickhouse" + +func clickHouseWriteState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + mfst manifest.Manifest, +) map[string][]metav1.Condition { + out := map[string][]metav1.Condition{} + for key, spec := range wandb.Spec.ClickHouse { + switch { + case spec.ManagedClickHouse != nil: + out[key] = managedClickHouseWriteState(ctx, client, wandb, spec.ManagedClickHouse, mfst) + case spec.ExternalClickHouse != nil: + out[key] = externalch.WriteState(ctx, client, wandb, key, spec.ExternalClickHouse) + } + } + return out +} + +func clickHouseReadState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + conditions map[string][]metav1.Condition, +) (map[string][]metav1.Condition, map[string]*apiv2.ClickHouseConnection) { + outConds := map[string][]metav1.Condition{} + outConns := map[string]*apiv2.ClickHouseConnection{} + for key, spec := range wandb.Spec.ClickHouse { + switch { + case spec.ManagedClickHouse != nil: + outConds[key], outConns[key] = managedClickHouseReadState(ctx, client, wandb, spec.ManagedClickHouse, conditions[key]) + case spec.ExternalClickHouse != nil: + outConds[key], outConns[key] = externalch.ReadState(ctx, client, wandb, key, conditions[key]) + default: + outConds[key] = conditions[key] + } + } + return outConds, outConns +} + +func clickHouseInferStatus( + ctx context.Context, + client client.Client, + recorder record.EventRecorder, + wandb *apiv2.WeightsAndBiases, + conditions map[string][]metav1.Condition, + infraConns map[string]*apiv2.ClickHouseConnection, +) (ctrl.Result, error) { + if wandb.Status.ClickHouseStatus == nil { + wandb.Status.ClickHouseStatus = map[string]apiv2.ClickHouseInfraStatus{} + } + var results []ctrl.Result + var firstErr error + for key, spec := range wandb.Spec.ClickHouse { + var res ctrl.Result + var err error + switch { + case spec.ManagedClickHouse != nil: + res, err = managedClickHouseInferStatus(ctx, client, recorder, wandb, key, conditions[key], infraConns[key]) + case spec.ExternalClickHouse != nil: + res, err = externalClickHouseInferStatus(ctx, client, wandb, key, conditions[key], infraConns[key]) + } + results = append(results, res) + if err != nil && firstErr == nil { + firstErr = err + } + } + return consolidateResults(results), firstErr +} + +func runClickHouseRetentionFinalizer(ctx context.Context, c client.Client, wandb *apiv2.WeightsAndBiases, key string, spec apiv2.ClickHouseSpec) error { + switch wandb.GetRetentionPolicy(clickHouseInstanceInfraSpec(spec)).OnDelete { + case apiv2.PurgeOnDelete: + return clickHousePurgeFinalizer(ctx, c, wandb, key, spec) + case apiv2.DetachOnDelete: + return clickHouseDetachFinalizer(ctx, c, wandb, key, spec) + } + return nil +} + +func clickHouseInstanceInfraSpec(spec apiv2.ClickHouseSpec) apiv2.ManagedInfraSpec { + if spec.ManagedClickHouse != nil { + return spec.ManagedClickHouse.ManagedInfraSpec + } + return apiv2.ManagedInfraSpec{} +} + +func clickHousePurgeFinalizer( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + key string, + spec apiv2.ClickHouseSpec, +) error { + if managed := spec.ManagedClickHouse; managed != nil { + specNamespacedName := managedClickHouseSpecNamespacedName(managed) + onDeleteRule := altinity.ToClickHouseOnDeleteRule(wandb, wandb.GetRetentionPolicy(managed.ManagedInfraSpec)) + return altinity.PurgeFinalizer(ctx, client, specNamespacedName, onDeleteRule) + } + if spec.ExternalClickHouse != nil { + return externalch.DeleteConnectionSecret(ctx, client, wandb, key) + } + return nil +} + +func clickHouseDetachFinalizer( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + _ string, + spec apiv2.ClickHouseSpec, +) error { + managed := spec.ManagedClickHouse + if managed == nil { + return nil + } + specNamespacedName := managedClickHouseSpecNamespacedName(managed) + return altinity.DetachFinalizer(ctx, client, specNamespacedName, wandb) +} + +// managed + +func managedClickHouseWriteState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedClickHouseSpec, + mfst manifest.Manifest, +) []metav1.Condition { + log := ctrl.LoggerFrom(ctx) + + // Altinity swallows the apiserver's rejection of over-long derived names, so + // fail the status loudly here; also covers CRs that predate admission checks. + if err := altinity.ValidateDerivedNames(spec); err != nil { + log.Error(err, "managed ClickHouse name cannot be deployed") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.InvalidNameReason, + Message: err.Error(), + }, + { + Type: altinity.ClickHouseCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.InvalidNameReason, + Message: err.Error(), + }, + } + } + + // ClickHouse table data lives in the object store: use the "clickhouse" + // instance when provisioned, otherwise the default instance. + objStoreStatus, _ := apiv2.ResolveInstance(wandb.Status.ObjectStoreStatus, clickHouseObjectStoreInstance) + objStoreSpec, _ := apiv2.ResolveInstance(wandb.Spec.ObjectStore, clickHouseObjectStoreInstance) + waitForObjectStore := objStoreSpec.ManagedObjectStore != nil + + // Resolve the bucket connection; wait and requeue if it isn't ready yet. + objStorage, err := altinity.ResolveObjectStorage(ctx, client, spec, &objStoreStatus.Connection) + if err != nil { + log.Error(err, "object storage not ready for ClickHouse") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.PendingCreateReason, + }, + { + Type: altinity.ClickHouseCustomResourceType, + Status: metav1.ConditionFalse, + Reason: common.PendingCreateReason, + }, + } + } + + // Translate the Keeper and ClickHouse CRs; WriteState writes Keeper first. + desiredKeeper, err := keeper.ToKeeperVendorSpec(ctx, wandb, spec, client.Scheme(), altinity.KeeperNsName(spec)) + if err != nil { + log.Error(err, "failed to translate Keeper spec to vendor spec") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ControllerErrorReason, + }, + } + } + + desired, err := altinity.ToClickHouseVendorSpec(ctx, wandb, spec, client.Scheme(), objStorage, waitForObjectStore, mfst) + if err != nil { + log.Error(err, "failed to translate ClickHouse spec to vendor spec") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ControllerErrorReason, + }, + } + } + + specNamespacedName := managedClickHouseSpecNamespacedName(spec) + + if conditions := altinity.CheckDetached(ctx, client, specNamespacedName, wandb.GetUID()); conditions != nil { + return conditions + } + + results := make([]metav1.Condition, 0) + results = append(results, altinity.WriteState(ctx, client, specNamespacedName, desiredKeeper, desired)...) + + return results +} + +func managedClickHouseReadState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedClickHouseSpec, + newConditions []metav1.Condition, +) ([]metav1.Condition, *apiv2.ClickHouseConnection) { + specNamespacedName := managedClickHouseSpecNamespacedName(spec) + onDeleteRule := altinity.ToClickHouseOnDeleteRule(wandb, wandb.GetRetentionPolicy(spec.ManagedInfraSpec)) + readConditions, newInfraConn := altinity.ReadState(ctx, client, specNamespacedName, wandb, onDeleteRule) + newConditions = append(newConditions, readConditions...) + + // Keeper readiness gates ClickHouse readiness (see inferInfraState). + newConditions = append(newConditions, keeper.ReadState(ctx, client, altinity.KeeperNsName(spec))...) + + return newConditions, newInfraConn +} + +func managedClickHouseInferStatus( + ctx context.Context, + client client.Client, + recorder record.EventRecorder, + wandb *apiv2.WeightsAndBiases, + key string, + newConditions []metav1.Condition, + newInfraConn *apiv2.ClickHouseConnection, +) (ctrl.Result, error) { + statusBefore := wandb.DeepCopy().Status + enabled := true + oldStatus := wandb.Status.ClickHouseStatus[key] + oldConditions := oldStatus.Conditions + oldInfraConn := oldStatus.Connection + + updatedStatus, events, ctrlResult := altinity.ComputeStatus( + ctx, + enabled, + oldConditions, + newConditions, + utils.Coalesce(newInfraConn, &oldInfraConn), + wandb.Generation, + ) + for _, e := range events { + recorder.Event(wandb, e.Type, e.Reason, e.Message) + } + wandb.Status.ClickHouseStatus[key] = updatedStatus + err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore) + + return ctrlResult, err +} + +// external + +func externalClickHouseInferStatus(ctx context.Context, c client.Client, wandb *apiv2.WeightsAndBiases, key string, newConditions []metav1.Condition, newInfraConn *apiv2.ClickHouseConnection) (ctrl.Result, error) { + statusBefore := wandb.DeepCopy().Status + oldStatus := wandb.Status.ClickHouseStatus[key] + oldInfraConn := oldStatus.Connection + state, ready, updatedConditions := external.InferExternalStatus(oldStatus.Conditions, newConditions, wandb.Generation, newInfraConn != nil) + conn := utils.Coalesce(newInfraConn, &oldInfraConn) + + wandb.Status.ClickHouseStatus[key] = apiv2.ClickHouseInfraStatus{ + WBInfraStatus: apiv2.WBInfraStatus{Ready: ready, State: state, Conditions: updatedConditions}, + Connection: *conn, + } + return ctrl.Result{}, updateWandbStatusIfChanged(ctx, c, wandb, statusBefore) +} + +// helpers + +func managedClickHouseSpecNamespacedName(spec *apiv2.ManagedClickHouseSpec) types.NamespacedName { + return types.NamespacedName{ + Namespace: spec.Namespace, + Name: spec.Name, + } +} diff --git a/internal/controller/reconciler/custom_ca.go b/internal/controller/reconciler/custom_ca.go new file mode 100644 index 00000000..8dfe6c31 --- /dev/null +++ b/internal/controller/reconciler/custom_ca.go @@ -0,0 +1,477 @@ +package reconciler + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "sort" + + apiv2 "github.com/wandb/operator/api/v2" + corev1 "k8s.io/api/core/v1" + apiErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + customCACertsChecksumAnnotation = "weightsandbiases.apps.wandb.com/ca-certs-checksum" + + customCACertsRootVolumeName = "wandb-ca-certs-root" + customCACertsInlineVolumeName = "wandb-ca-certs" + customCACertsConfigMapVolumeName = "wandb-ca-certs-user" + + customCACertsRootMountPath = "/usr/local/share/ca-certificates/" + customCACertsInlineMountPath = "/usr/local/share/ca-certificates/inline" + customCACertsConfigMapMountPath = "/usr/local/share/ca-certificates/configmap" + + mysqlCACertVolumeName = "mysql-ca" + mysqlCACertPath = "/etc/ssl/certs/mysql_ca.pem" + mysqlCACertFileName = "mysql_ca.pem" + + mysqlSSLCertVolumeName = "mysql-ssl-cert" + mysqlSSLCertPath = "/etc/ssl/certs/mysql_ssl_cert.pem" + mysqlSSLCertFileName = "mysql_ssl_cert.pem" + + mysqlSSLKeyVolumeName = "mysql-ssl-key" + mysqlSSLKeyPath = "/etc/ssl/certs/mysql_ssl_key.pem" + mysqlSSLKeyFileName = "mysql_ssl_key.pem" + + redisCACertVolumeName = "redis-ca" + redisCACertPath = "/etc/ssl/certs/redis_ca.pem" + redisCACertFileName = "redis_ca.pem" +) + +var customCACertsEnvVars = []corev1.EnvVar{ + {Name: "SSL_CERT_FILE", Value: "/etc/ssl/certs/ca-certificates.crt"}, + {Name: "SSL_CERT_DIR", Value: "/etc/ssl/certs"}, + {Name: "REQUESTS_CA_BUNDLE", Value: "/etc/ssl/certs/ca-certificates.crt"}, +} + +func customCACertsConfigMapName(wandb *apiv2.WeightsAndBiases) string { + return fmt.Sprintf("%s-ca-certs", wandb.Name) +} + +func hasGlobalCustomCACertConfig(wandb *apiv2.WeightsAndBiases) bool { + return len(wandb.Spec.Global.CustomCACerts) > 0 || wandb.Spec.Global.CACertsConfigMap != "" +} + +// defaultMySQLConnection returns the default MySQL instance's connection. The +// app's TLS env vars (MYSQL_CA_CERT_PATH etc.) are singular, so only the +// default instance's certificate material is mounted. +func defaultMySQLConnection(wandb *apiv2.WeightsAndBiases) apiv2.MysqlConnection { + status, _ := apiv2.ResolveInstance(wandb.Status.MySQLStatus, "") + return status.Connection +} + +// defaultRedisConnection returns the default Redis instance's connection; see +// defaultMySQLConnection. +func defaultRedisConnection(wandb *apiv2.WeightsAndBiases) apiv2.RedisConnection { + status, _ := apiv2.ResolveInstance(wandb.Status.RedisStatus, "") + return status.Connection +} + +func secretSelectorConfigured(sel corev1.SecretKeySelector) bool { + return sel.Name != "" && sel.Key != "" +} + +func hasOwnerReference(obj ctrlClient.Object, owner ctrlClient.Object) bool { + ownerUID := owner.GetUID() + for _, ref := range obj.GetOwnerReferences() { + if ownerUID != "" && ref.UID == ownerUID { + return true + } + } + return false +} + +func reconcileCustomCACerts(ctx context.Context, c ctrlClient.Client, wandb *apiv2.WeightsAndBiases) error { + nsName := types.NamespacedName{Name: customCACertsConfigMapName(wandb), Namespace: wandb.Namespace} + actual := &corev1.ConfigMap{} + err := c.Get(ctx, nsName, actual) + if err != nil && !apiErrors.IsNotFound(err) { + return err + } + + if len(wandb.Spec.Global.CustomCACerts) == 0 { + if apiErrors.IsNotFound(err) { + return nil + } + if !hasOwnerReference(actual, wandb) { + return nil + } + return c.Delete(ctx, actual) + } + + data := make(map[string]string, len(wandb.Spec.Global.CustomCACerts)) + for i, pem := range wandb.Spec.Global.CustomCACerts { + data[fmt.Sprintf("customCA%d.crt", i)] = pem + } + + desired := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: nsName.Name, + Namespace: nsName.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + "app.kubernetes.io/part-of": "wandb", + }, + }, + Data: data, + } + if err := controllerutil.SetOwnerReference(wandb, desired, c.Scheme()); err != nil { + return err + } + + if apiErrors.IsNotFound(err) { + return c.Create(ctx, desired) + } + desired.ResourceVersion = actual.ResourceVersion + return c.Update(ctx, desired) +} + +func applyCustomCACertsToWorkload( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, + envVars []corev1.EnvVar, + volumes []corev1.Volume, + volumeMounts []corev1.VolumeMount, +) ([]corev1.EnvVar, []corev1.Volume, []corev1.VolumeMount, string, error) { + mysqlConn := defaultMySQLConnection(wandb) + redisConn := defaultRedisConnection(wandb) + + if hasGlobalCustomCACertConfig(wandb) { + envVars = appendMissingEnvVars(envVars, customCACertsEnvVars) + volumes = upsertVolume(volumes, corev1.Volume{ + Name: customCACertsRootVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }) + volumeMounts = upsertVolumeMount(volumeMounts, corev1.VolumeMount{ + Name: customCACertsRootVolumeName, + MountPath: customCACertsRootMountPath, + ReadOnly: false, + }) + + if len(wandb.Spec.Global.CustomCACerts) > 0 { + volumes = upsertVolume(volumes, corev1.Volume{ + Name: customCACertsInlineVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: customCACertsConfigMapName(wandb)}, + }, + }, + }) + volumeMounts = upsertVolumeMount(volumeMounts, corev1.VolumeMount{ + Name: customCACertsInlineVolumeName, + MountPath: customCACertsInlineMountPath, + ReadOnly: true, + }) + } + + if wandb.Spec.Global.CACertsConfigMap != "" { + volumes = upsertVolume(volumes, corev1.Volume{ + Name: customCACertsConfigMapVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: wandb.Spec.Global.CACertsConfigMap}, + Optional: boolPtr(true), + }, + }, + }) + volumeMounts = upsertVolumeMount(volumeMounts, corev1.VolumeMount{ + Name: customCACertsConfigMapVolumeName, + MountPath: customCACertsConfigMapMountPath, + ReadOnly: true, + }) + } + } + + if hasValue, err := secretSelectorHasValue(ctx, c, wandb.Namespace, mysqlConn.SslCa); err != nil { + return nil, nil, nil, "", err + } else if hasValue { + envVars = appendMissingEnvVars(envVars, []corev1.EnvVar{{Name: "MYSQL_CA_CERT_PATH", Value: mysqlCACertPath}}) + volumes = upsertVolume(volumes, corev1.Volume{ + Name: mysqlCACertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: secretCACertVolumeSource(mysqlConn.SslCa, mysqlCACertFileName), + }, + }) + volumeMounts = upsertVolumeMount(volumeMounts, corev1.VolumeMount{ + Name: mysqlCACertVolumeName, + MountPath: mysqlCACertPath, + SubPath: mysqlCACertFileName, + ReadOnly: true, + }) + } + if hasValue, err := secretSelectorHasValue(ctx, c, wandb.Namespace, mysqlConn.SslCert); err != nil { + return nil, nil, nil, "", err + } else if hasValue { + volumes = upsertVolume(volumes, corev1.Volume{ + Name: mysqlSSLCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: secretCACertVolumeSource(mysqlConn.SslCert, mysqlSSLCertFileName), + }, + }) + volumeMounts = upsertVolumeMount(volumeMounts, corev1.VolumeMount{ + Name: mysqlSSLCertVolumeName, + MountPath: mysqlSSLCertPath, + SubPath: mysqlSSLCertFileName, + ReadOnly: true, + }) + } + if hasValue, err := secretSelectorHasValue(ctx, c, wandb.Namespace, mysqlConn.SslKey); err != nil { + return nil, nil, nil, "", err + } else if hasValue { + volumes = upsertVolume(volumes, corev1.Volume{ + Name: mysqlSSLKeyVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: secretCACertVolumeSource(mysqlConn.SslKey, mysqlSSLKeyFileName), + }, + }) + volumeMounts = upsertVolumeMount(volumeMounts, corev1.VolumeMount{ + Name: mysqlSSLKeyVolumeName, + MountPath: mysqlSSLKeyPath, + SubPath: mysqlSSLKeyFileName, + ReadOnly: true, + }) + } + + if hasValue, err := secretSelectorHasValue(ctx, c, wandb.Namespace, redisConn.SslCa); err != nil { + return nil, nil, nil, "", err + } else if hasValue { + volumes = upsertVolume(volumes, corev1.Volume{ + Name: redisCACertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: secretCACertVolumeSource(redisConn.SslCa, redisCACertFileName), + }, + }) + volumeMounts = upsertVolumeMount(volumeMounts, corev1.VolumeMount{ + Name: redisCACertVolumeName, + MountPath: redisCACertPath, + SubPath: redisCACertFileName, + ReadOnly: true, + }) + } + + checksum, err := customCACertsChecksum(ctx, c, wandb) + if err != nil { + return nil, nil, nil, "", err + } + return envVars, volumes, volumeMounts, checksum, nil +} + +func setCustomCACertsChecksumAnnotation(podTemplate *corev1.PodTemplateSpec, checksum string) { + annotations := podTemplate.GetAnnotations() + if checksum == "" { + if annotations == nil { + return + } + delete(annotations, customCACertsChecksumAnnotation) + if len(annotations) == 0 { + annotations = nil + } + podTemplate.SetAnnotations(annotations) + return + } + if annotations == nil { + annotations = map[string]string{} + } + annotations[customCACertsChecksumAnnotation] = checksum + podTemplate.SetAnnotations(annotations) +} + +func secretCACertVolumeSource(sel corev1.SecretKeySelector, fileName string) *corev1.SecretVolumeSource { + return &corev1.SecretVolumeSource{ + SecretName: sel.Name, + Items: []corev1.KeyToPath{{ + Key: sel.Key, + Path: fileName, + }}, + Optional: sel.Optional, + } +} + +func upsertVolume(volumes []corev1.Volume, volume corev1.Volume) []corev1.Volume { + for i := range volumes { + if volumes[i].Name == volume.Name { + volumes[i] = volume + return volumes + } + } + return append(volumes, volume) +} + +func upsertVolumeMount(volumeMounts []corev1.VolumeMount, mount corev1.VolumeMount) []corev1.VolumeMount { + for i := range volumeMounts { + if volumeMounts[i].Name == mount.Name { + volumeMounts[i] = mount + return volumeMounts + } + } + return append(volumeMounts, mount) +} + +func secretSelectorHasValue(ctx context.Context, c ctrlClient.Client, namespace string, sel corev1.SecretKeySelector) (bool, error) { + if !secretSelectorConfigured(sel) { + return false, nil + } + + secret := &corev1.Secret{} + err := c.Get(ctx, types.NamespacedName{Name: sel.Name, Namespace: namespace}, secret) + if apiErrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + + if _, ok := secret.Data[sel.Key]; ok { + return true, nil + } + if _, ok := secret.StringData[sel.Key]; ok { + return true, nil + } + return false, nil +} + +func customCACertsChecksum(ctx context.Context, c ctrlClient.Client, wandb *apiv2.WeightsAndBiases) (string, error) { + mysqlConn := defaultMySQLConnection(wandb) + redisConn := defaultRedisConnection(wandb) + + hasMySQLCA, err := secretSelectorHasValue(ctx, c, wandb.Namespace, mysqlConn.SslCa) + if err != nil { + return "", err + } + hasMySQLCert, err := secretSelectorHasValue(ctx, c, wandb.Namespace, mysqlConn.SslCert) + if err != nil { + return "", err + } + hasMySQLKey, err := secretSelectorHasValue(ctx, c, wandb.Namespace, mysqlConn.SslKey) + if err != nil { + return "", err + } + hasRedisCA, err := secretSelectorHasValue(ctx, c, wandb.Namespace, redisConn.SslCa) + if err != nil { + return "", err + } + + if !hasGlobalCustomCACertConfig(wandb) && !hasMySQLCA && !hasMySQLCert && !hasMySQLKey && !hasRedisCA { + return "", nil + } + + hash := sha256.New() + for i, pem := range wandb.Spec.Global.CustomCACerts { + _, _ = fmt.Fprintf(hash, "inline:%d:%s\n", i, pem) + } + + if wandb.Spec.Global.CACertsConfigMap != "" { + _, _ = fmt.Fprintf(hash, "configmap:%s\n", wandb.Spec.Global.CACertsConfigMap) + if err := hashConfigMapData(ctx, c, wandb.Namespace, wandb.Spec.Global.CACertsConfigMap, hashWriteString(hash)); err != nil { + return "", err + } + } + + if sel := mysqlConn.SslCa; hasMySQLCA { + _, _ = fmt.Fprintf(hash, "mysql:%s/%s\n", sel.Name, sel.Key) + if err := hashSecretKeyData(ctx, c, wandb.Namespace, sel, hashWriteString(hash)); err != nil { + return "", err + } + } + if sel := mysqlConn.SslCert; hasMySQLCert { + _, _ = fmt.Fprintf(hash, "mysql-cert:%s/%s\n", sel.Name, sel.Key) + if err := hashSecretKeyData(ctx, c, wandb.Namespace, sel, hashWriteString(hash)); err != nil { + return "", err + } + } + if sel := mysqlConn.SslKey; hasMySQLKey { + _, _ = fmt.Fprintf(hash, "mysql-key:%s/%s\n", sel.Name, sel.Key) + if err := hashSecretKeyData(ctx, c, wandb.Namespace, sel, hashWriteString(hash)); err != nil { + return "", err + } + } + + if sel := redisConn.SslCa; hasRedisCA { + _, _ = fmt.Fprintf(hash, "redis:%s/%s\n", sel.Name, sel.Key) + if err := hashSecretKeyData(ctx, c, wandb.Namespace, sel, hashWriteString(hash)); err != nil { + return "", err + } + } + + return hex.EncodeToString(hash.Sum(nil)), nil +} + +type writeStringFunc func(string) + +func hashWriteString(hash interface{ Write([]byte) (int, error) }) writeStringFunc { + return func(s string) { + _, _ = hash.Write([]byte(s)) + } +} + +func hashConfigMapData(ctx context.Context, c ctrlClient.Client, namespace, name string, write writeStringFunc) error { + configMap := &corev1.ConfigMap{} + err := c.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, configMap) + if apiErrors.IsNotFound(err) { + write("missing-configmap\n") + return nil + } + if err != nil { + return err + } + + keys := make([]string, 0, len(configMap.Data)+len(configMap.BinaryData)) + for k := range configMap.Data { + keys = append(keys, "data:"+k) + } + for k := range configMap.BinaryData { + keys = append(keys, "binary:"+k) + } + sort.Strings(keys) + for _, typedKey := range keys { + write(typedKey) + write("=") + switch { + case len(typedKey) > len("data:") && typedKey[:len("data:")] == "data:": + write(configMap.Data[typedKey[len("data:"):]]) + case len(typedKey) > len("binary:") && typedKey[:len("binary:")] == "binary:": + write(string(configMap.BinaryData[typedKey[len("binary:"):]])) + } + write("\n") + } + return nil +} + +func hashSecretKeyData(ctx context.Context, c ctrlClient.Client, namespace string, sel corev1.SecretKeySelector, write writeStringFunc) error { + secret := &corev1.Secret{} + err := c.Get(ctx, types.NamespacedName{Name: sel.Name, Namespace: namespace}, secret) + if apiErrors.IsNotFound(err) { + write("missing-secret\n") + return nil + } + if err != nil { + return err + } + + if data, ok := secret.Data[sel.Key]; ok { + write(string(data)) + write("\n") + return nil + } + if stringData, ok := secret.StringData[sel.Key]; ok { + write(stringData) + write("\n") + return nil + } + write("missing-key\n") + return nil +} + +func boolPtr(v bool) *bool { + return &v +} diff --git a/internal/controller/reconciler/custom_ca_test.go b/internal/controller/reconciler/custom_ca_test.go new file mode 100644 index 00000000..ea43e3df --- /dev/null +++ b/internal/controller/reconciler/custom_ca_test.go @@ -0,0 +1,270 @@ +package reconciler + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + apiv2 "github.com/wandb/operator/api/v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func customCATestClient(t *testing.T, objects ...ctrlClient.Object) *fake.ClientBuilder { + t.Helper() + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + require.NoError(t, apiv2.AddToScheme(scheme)) + + builder := fake.NewClientBuilder().WithScheme(scheme) + if len(objects) > 0 { + builder.WithObjects(objects...) + } + return builder +} + +func TestReconcileCustomCACertsCreatesInlineConfigMap(t *testing.T) { + wandb := &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{APIVersion: "apps.wandb.com/v2", Kind: "WeightsAndBiases"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "default", + UID: "wandb-uid", + }, + Spec: apiv2.WeightsAndBiasesSpec{ + Global: apiv2.GlobalSpec{ + CustomCACerts: []string{"---cert-one---", "---cert-two---"}, + }, + }, + } + builder := customCATestClient(t, wandb) + client := builder.Build() + + require.NoError(t, reconcileCustomCACerts(context.Background(), client, wandb)) + + var cm corev1.ConfigMap + require.NoError(t, client.Get(context.Background(), types.NamespacedName{Name: "wandb-ca-certs", Namespace: "default"}, &cm)) + require.Equal(t, "---cert-one---", cm.Data["customCA0.crt"]) + require.Equal(t, "---cert-two---", cm.Data["customCA1.crt"]) + require.Len(t, cm.OwnerReferences, 1) + require.Equal(t, "wandb", cm.OwnerReferences[0].Name) +} + +func TestReconcileCustomCACertsDoesNotDeleteUnownedGeneratedConfigMap(t *testing.T) { + wandb := &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{APIVersion: "apps.wandb.com/v2", Kind: "WeightsAndBiases"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "default", + UID: "wandb-uid", + }, + } + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb-ca-certs", Namespace: "default"}, + Data: map[string]string{"user.crt": "---user---"}, + } + builder := customCATestClient(t, wandb, configMap) + client := builder.Build() + + require.NoError(t, reconcileCustomCACerts(context.Background(), client, wandb)) + + var cm corev1.ConfigMap + require.NoError(t, client.Get(context.Background(), types.NamespacedName{Name: "wandb-ca-certs", Namespace: "default"}, &cm)) + require.Equal(t, "---user---", cm.Data["user.crt"]) +} + +func TestApplyCustomCACertsToWorkloadAddsGlobalAndInfraMounts(t *testing.T) { + wandb := &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{APIVersion: "apps.wandb.com/v2", Kind: "WeightsAndBiases"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "default", + }, + Spec: apiv2.WeightsAndBiasesSpec{ + Global: apiv2.GlobalSpec{ + CustomCACerts: []string{"---inline---"}, + CACertsConfigMap: "user-ca-certs", + }, + }, + Status: apiv2.WeightsAndBiasesStatus{ + MySQLStatus: map[string]apiv2.MysqlInfraStatus{ + apiv2.DefaultInstanceName: apiv2.MysqlInfraStatus{ + Connection: apiv2.MysqlConnection{ + SslCa: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "wandb-mysql-connection"}, + Key: "SslCa", + }, + SslCert: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "wandb-mysql-connection"}, + Key: "SslCert", + }, + SslKey: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "wandb-mysql-connection"}, + Key: "SslKey", + }, + }, + }, + }, + RedisStatus: map[string]apiv2.RedisInfraStatus{ + apiv2.DefaultInstanceName: apiv2.RedisInfraStatus{ + Connection: apiv2.RedisConnection{ + SslCa: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "wandb-redis-connection"}, + Key: "SslCa", + Optional: ptr.To(true), + }, + }, + }, + }, + }, + } + mysqlSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb-mysql-connection", Namespace: "default"}, + Data: map[string][]byte{ + "SslCa": []byte("---mysql-ca---"), + "SslCert": []byte("---mysql-cert---"), + "SslKey": []byte("---mysql-key---"), + }, + } + redisSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb-redis-connection", Namespace: "default"}, + Data: map[string][]byte{"SslCa": []byte("---redis-ca---")}, + } + userCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "user-ca-certs", Namespace: "default"}, + Data: map[string]string{"corp.crt": "---corp---"}, + } + builder := customCATestClient(t, wandb, mysqlSecret, redisSecret, userCM) + client := builder.Build() + + envs, volumes, mounts, checksum, err := applyCustomCACertsToWorkload(context.Background(), client, wandb, nil, nil, nil) + require.NoError(t, err) + require.NotEmpty(t, checksum) + + requireContainsEnv(t, envs, "SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt") + requireContainsEnv(t, envs, "SSL_CERT_DIR", "/etc/ssl/certs") + requireContainsEnv(t, envs, "REQUESTS_CA_BUNDLE", "/etc/ssl/certs/ca-certificates.crt") + requireContainsEnv(t, envs, "MYSQL_CA_CERT_PATH", mysqlCACertPath) + + requireVolume(t, volumes, customCACertsRootVolumeName) + requireVolume(t, volumes, customCACertsInlineVolumeName) + requireVolume(t, volumes, customCACertsConfigMapVolumeName) + requireVolume(t, volumes, mysqlCACertVolumeName) + requireVolume(t, volumes, mysqlSSLCertVolumeName) + requireVolume(t, volumes, mysqlSSLKeyVolumeName) + requireVolume(t, volumes, redisCACertVolumeName) + + requireMount(t, mounts, customCACertsRootVolumeName, customCACertsRootMountPath) + requireMount(t, mounts, customCACertsInlineVolumeName, customCACertsInlineMountPath) + requireMount(t, mounts, customCACertsConfigMapVolumeName, customCACertsConfigMapMountPath) + requireMount(t, mounts, mysqlCACertVolumeName, mysqlCACertPath) + requireMount(t, mounts, mysqlSSLCertVolumeName, mysqlSSLCertPath) + requireMount(t, mounts, mysqlSSLKeyVolumeName, mysqlSSLKeyPath) + requireMount(t, mounts, redisCACertVolumeName, redisCACertPath) + + podTemplate := &corev1.PodTemplateSpec{} + setCustomCACertsChecksumAnnotation(podTemplate, checksum) + require.Equal(t, checksum, podTemplate.Annotations[customCACertsChecksumAnnotation]) +} + +func TestApplyCustomCACertsToWorkloadSkipsMissingOptionalInfraKeys(t *testing.T) { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "default", + }, + Status: apiv2.WeightsAndBiasesStatus{ + MySQLStatus: map[string]apiv2.MysqlInfraStatus{ + apiv2.DefaultInstanceName: apiv2.MysqlInfraStatus{ + Connection: apiv2.MysqlConnection{ + SslCa: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "wandb-mysql-connection"}, + Key: "SslCa", + Optional: ptr.To(true), + }, + }, + }, + }, + RedisStatus: map[string]apiv2.RedisInfraStatus{ + apiv2.DefaultInstanceName: apiv2.RedisInfraStatus{ + Connection: apiv2.RedisConnection{ + SslCa: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "wandb-redis-connection"}, + Key: "SslCa", + Optional: ptr.To(true), + }, + }, + }, + }, + }, + } + mysqlSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb-mysql-connection", Namespace: "default"}, + Data: map[string][]byte{"url": []byte("mysql://user:pass@db:3306/wandb")}, + } + redisSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb-redis-connection", Namespace: "default"}, + Data: map[string][]byte{"url": []byte("redis://redis:6379")}, + } + builder := customCATestClient(t, wandb, mysqlSecret, redisSecret) + client := builder.Build() + + envs, volumes, mounts, checksum, err := applyCustomCACertsToWorkload(context.Background(), client, wandb, nil, nil, nil) + require.NoError(t, err) + require.Empty(t, checksum) + requireNoEnv(t, envs, "MYSQL_CA_CERT_PATH") + requireNoVolume(t, volumes, mysqlCACertVolumeName) + requireNoVolume(t, volumes, redisCACertVolumeName) + require.Empty(t, mounts) +} + +func requireContainsEnv(t *testing.T, envs []corev1.EnvVar, name, value string) { + t.Helper() + for _, env := range envs { + if env.Name == name { + require.Equal(t, value, env.Value) + return + } + } + t.Fatalf("env var %q not found in %+v", name, envs) +} + +func requireNoEnv(t *testing.T, envs []corev1.EnvVar, name string) { + t.Helper() + for _, env := range envs { + require.NotEqual(t, name, env.Name) + } +} + +func requireVolume(t *testing.T, volumes []corev1.Volume, name string) { + t.Helper() + for _, volume := range volumes { + if volume.Name == name { + return + } + } + t.Fatalf("volume %q not found in %+v", name, volumes) +} + +func requireNoVolume(t *testing.T, volumes []corev1.Volume, name string) { + t.Helper() + for _, volume := range volumes { + require.NotEqual(t, name, volume.Name) + } +} + +func requireMount(t *testing.T, mounts []corev1.VolumeMount, name, mountPath string) { + t.Helper() + for _, mount := range mounts { + if mount.Name == name { + require.Equal(t, mountPath, mount.MountPath) + return + } + } + t.Fatalf("volume mount %q not found in %+v", name, mounts) +} diff --git a/internal/controller/reconciler/gateway.go b/internal/controller/reconciler/gateway.go new file mode 100644 index 00000000..bb371f37 --- /dev/null +++ b/internal/controller/reconciler/gateway.go @@ -0,0 +1,360 @@ +package reconciler + +import ( + "context" + "fmt" + "net/url" + "strconv" + + nginxGatewayv1alpha1 "github.com/nginx/nginx-gateway-fabric/apis/v1alpha1" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/logx" + "github.com/wandb/operator/pkg/utils" + apiErrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + controllerruntime "sigs.k8s.io/controller-runtime" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +func reconcileGateway(ctx context.Context, c ctrlClient.Client, wandb *apiv2.WeightsAndBiases) error { + ctx, logger := logx.WithSlog(ctx, "Gateway") + logger.Info("Reconciling Gateway") + gwConfig := wandb.Spec.Networking.GatewayAPI + if gwConfig == nil { + return nil + } + + if !gwConfig.Gateway.Managed { + if gwConfig.Gateway.GatewayRef == nil { + return fmt.Errorf("gatewayAPI.gateway.gatewayRef is required when managed=false") + } + gw, err := validateExternalGatewayExists(ctx, c, wandb, gwConfig.Gateway.GatewayRef) + if err != nil { + return err + } + wandb.Status.GatewayStatus = summarizeGatewayStatus(gw) + return nil + } + + gatewayName := fmt.Sprintf("%s-gateway", wandb.Name) + annotations := utils.MergeMapsStringString(make(map[string]string), wandb.Spec.Networking.Annotations) + + if wandb.Spec.Networking.TLS != nil && wandb.Spec.Networking.TLS.CertManager != nil { + cm := wandb.Spec.Networking.TLS.CertManager + if cm.ClusterIssuer != "" { + annotations["cert-manager.io/cluster-issuer"] = cm.ClusterIssuer + } + if cm.Issuer != "" { + annotations["cert-manager.io/issuer"] = cm.Issuer + } + } + + desired := &gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: gatewayName, + Namespace: wandb.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + }, + Annotations: annotations, + }, + Spec: gatewayv1.GatewaySpec{ + GatewayClassName: gatewayv1.ObjectName(*gwConfig.Gateway.GatewayClassName), + }, + } + + if wandb.Spec.Networking.GatewayAPI.Gateway.InfrastructureAnnotations != nil { + if desired.Spec.Infrastructure == nil { + desired.Spec.Infrastructure = &gatewayv1.GatewayInfrastructure{} + } + desired.Spec.Infrastructure.Annotations = map[gatewayv1.AnnotationKey]gatewayv1.AnnotationValue{} + for k, v := range wandb.Spec.Networking.GatewayAPI.Gateway.InfrastructureAnnotations { + desired.Spec.Infrastructure.Annotations[gatewayv1.AnnotationKey(k)] = gatewayv1.AnnotationValue(v) + } + } + + if len(gwConfig.Gateway.Listeners) > 0 { + desired.Spec.Listeners = buildListenersFromConfig(gwConfig.Gateway.Listeners, wandb) + } else { + desired.Spec.Listeners = buildDefaultListeners(wandb) + } + + if err := controllerutil.SetOwnerReference(wandb, desired, c.Scheme()); err != nil { + return err + } + + current := &gatewayv1.Gateway{} + err := c.Get(ctx, types.NamespacedName{Name: gatewayName, Namespace: wandb.Namespace}, current) + if err != nil { + if apiErrors.IsNotFound(err) { + if err := c.Create(ctx, desired); err != nil { + return err + } + wandb.Status.GatewayStatus = summarizeGatewayStatus(desired) + } else { + return err + } + } else { + desired.ResourceVersion = current.ResourceVersion + if err := c.Update(ctx, desired); err != nil { + return err + } + wandb.Status.GatewayStatus = summarizeGatewayStatus(current) + } + + if wandb.Status.GatewayStatus == nil { + wandb.Status.GatewayStatus = summarizeGatewayStatus(desired) + } + + maxSize := "0" + timeoutStr := "2m" + switch *wandb.Spec.Networking.GatewayAPI.Gateway.GatewayClassName { + case "nginx": + csp := &nginxGatewayv1alpha1.ClientSettingsPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: gatewayName, + Namespace: wandb.Namespace, + }, + } + _, err = controllerruntime.CreateOrUpdate(ctx, c, csp, func() error { + csp.Spec = nginxGatewayv1alpha1.ClientSettingsPolicySpec{ + Body: &nginxGatewayv1alpha1.ClientBody{ + MaxSize: (*nginxGatewayv1alpha1.Size)(&maxSize), + Timeout: (*nginxGatewayv1alpha1.Duration)(&timeoutStr), + }, + KeepAlive: nil, + TargetRef: gatewayv1alpha2.LocalPolicyTargetReference{ + Name: gatewayv1alpha2.ObjectName(gatewayName), + Kind: "Gateway", + Group: "gateway.networking.k8s.io", + }, + } + return nil + }) + if err != nil { + return err + } + } + + return nil +} + +func deleteGateway(ctx context.Context, c ctrlClient.Client, wandb *apiv2.WeightsAndBiases) error { + gatewayName := fmt.Sprintf("%s-gateway", wandb.Name) + gw := &gatewayv1.Gateway{} + if !utils.IsRegistered(c.Scheme(), gw) { + return nil + } + if err := c.Get(ctx, types.NamespacedName{Name: gatewayName, Namespace: wandb.Namespace}, gw); err != nil { + if apiErrors.IsNotFound(err) { + return nil + } + return err + } + return c.Delete(ctx, gw) +} + +func validateExternalGatewayExists( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, + ref *apiv2.GatewayReference, +) (*gatewayv1.Gateway, error) { + ns := ref.Namespace + if ns == "" { + ns = wandb.Namespace + } + gw := &gatewayv1.Gateway{} + if err := c.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: ns}, gw); err != nil { + if apiErrors.IsNotFound(err) { + return nil, fmt.Errorf("external Gateway %s/%s not found", ns, ref.Name) + } + return nil, err + } + return gw, nil +} + +func buildListenersFromConfig(listeners []apiv2.GatewayListener, wandb *apiv2.WeightsAndBiases) []gatewayv1.Listener { + var result []gatewayv1.Listener + for _, l := range listeners { + listener := gatewayv1.Listener{ + Name: gatewayv1.SectionName(l.Name), + Port: gatewayv1.PortNumber(l.Port), + Protocol: gatewayv1.ProtocolType(l.Protocol), + AllowedRoutes: buildAllowedRoutes(wandb), + } + if l.Hostname != nil { + h := gatewayv1.Hostname(*l.Hostname) + listener.Hostname = &h + } + if l.TLS != nil { + listener.TLS = buildListenerTLS(l.TLS, wandb) + } else if gatewayv1.ProtocolType(l.Protocol) == gatewayv1.HTTPSProtocolType { + listener.TLS = buildDefaultTLS(wandb) + } + result = append(result, listener) + } + return result +} + +func buildDefaultListeners(wandb *apiv2.WeightsAndBiases) []gatewayv1.Listener { + parsedURL, err := url.Parse(wandb.Spec.Wandb.Hostname) + hostname := gatewayv1.Hostname(parsedURL.Hostname()) + port, _ := strconv.Atoi(parsedURL.Port()) + listenerPort := gatewayv1.PortNumber(port) + if err != nil { + return nil + } + listener := gatewayv1.Listener{ + Name: gatewayv1.SectionName("http"), + Hostname: &hostname, + Port: listenerPort, + AllowedRoutes: buildAllowedRoutes(wandb), + } + if parsedURL.Scheme == "https" { + listener.Protocol = gatewayv1.HTTPSProtocolType + listener.TLS = buildDefaultTLS(wandb) + if parsedURL.Port() == "" { + listener.Port = gatewayv1.PortNumber(443) + } + } else { + listener.Protocol = gatewayv1.HTTPProtocolType + if parsedURL.Port() == "" { + listener.Port = gatewayv1.PortNumber(80) + } + } + + return []gatewayv1.Listener{listener} +} + +func buildListenerTLS(tlsConfig *apiv2.ListenerTLSConfig, wandb *apiv2.WeightsAndBiases) *gatewayv1.ListenerTLSConfig { + mode := gatewayv1.TLSModeTerminate + if tlsConfig.Mode != nil { + mode = gatewayv1.TLSModeType(*tlsConfig.Mode) + } + + listenerTLS := &gatewayv1.ListenerTLSConfig{ + Mode: &mode, + } + + if tlsConfig.CertificateRef != nil { + ref := gatewayv1.SecretObjectReference{ + Name: gatewayv1.ObjectName(tlsConfig.CertificateRef.Name), + } + if tlsConfig.CertificateRef.Namespace != "" { + ns := gatewayv1.Namespace(tlsConfig.CertificateRef.Namespace) + ref.Namespace = &ns + } + listenerTLS.CertificateRefs = []gatewayv1.SecretObjectReference{ref} + } else if wandb.Spec.Networking.TLS != nil && wandb.Spec.Networking.TLS.SecretName != "" { + listenerTLS.CertificateRefs = []gatewayv1.SecretObjectReference{{ + Name: gatewayv1.ObjectName(wandb.Spec.Networking.TLS.SecretName), + }} + } + + return listenerTLS +} + +func buildDefaultTLS(wandb *apiv2.WeightsAndBiases) *gatewayv1.ListenerTLSConfig { + mode := gatewayv1.TLSModeTerminate + listenerTLS := &gatewayv1.ListenerTLSConfig{ + Mode: &mode, + } + if wandb.Spec.Networking.TLS != nil && wandb.Spec.Networking.TLS.SecretName != "" { + listenerTLS.CertificateRefs = []gatewayv1.SecretObjectReference{{ + Name: gatewayv1.ObjectName(wandb.Spec.Networking.TLS.SecretName), + }} + } + return listenerTLS +} + +func parseHostname(rawHostname string) string { + parsed, err := url.Parse(rawHostname) + if err != nil { + return rawHostname + } + if parsed.Hostname() != "" { + return parsed.Hostname() + } + return rawHostname +} + +func buildAllowedRoutes(wandb *apiv2.WeightsAndBiases) *gatewayv1.AllowedRoutes { + from := gatewayv1.NamespacesFromSame + if requiresCrossNamespaceInfraRoutes(wandb) { + from = gatewayv1.NamespacesFromAll + } + + return &gatewayv1.AllowedRoutes{ + Namespaces: &gatewayv1.RouteNamespaces{ + From: &from, + }, + } +} + +func requiresCrossNamespaceInfraRoutes(wandb *apiv2.WeightsAndBiases) bool { + namespaces := []string{} + + for _, instance := range wandb.Spec.ObjectStore { + if spec := instance.ManagedObjectStore; spec != nil { + namespaces = append(namespaces, spec.Namespace) + } + } + for _, instance := range wandb.Spec.ClickHouse { + if spec := instance.ManagedClickHouse; spec != nil { + namespaces = append(namespaces, spec.Namespace) + } + } + for _, instance := range wandb.Spec.MySQL { + if spec := instance.ManagedMysql; spec != nil { + namespaces = append(namespaces, spec.Namespace) + } + } + for _, instance := range wandb.Spec.Redis { + if spec := instance.ManagedRedis; spec != nil { + namespaces = append(namespaces, spec.Namespace) + } + } + + for _, ns := range namespaces { + if ns != "" && ns != wandb.Namespace { + return true + } + } + + return false +} + +func summarizeGatewayStatus(gw *gatewayv1.Gateway) *apiv2.GatewayStatusSummary { + if gw == nil { + return nil + } + + summary := &apiv2.GatewayStatusSummary{ + Name: gw.Name, + Ready: isGatewayReady(gw.Status.Conditions), + GatewayRef: &apiv2.GatewayReference{ + Name: gw.Name, + Namespace: gw.Namespace, + }, + } + + for _, address := range gw.Status.Addresses { + summary.Addresses = append(summary.Addresses, address.Value) + } + + return summary +} + +func isGatewayReady(conditions []metav1.Condition) bool { + if apimeta.IsStatusConditionTrue(conditions, string(gatewayv1.GatewayConditionProgrammed)) { + return true + } + return apimeta.IsStatusConditionTrue(conditions, string(gatewayv1.GatewayConditionAccepted)) +} diff --git a/internal/controller/reconciler/infra_routes.go b/internal/controller/reconciler/infra_routes.go new file mode 100644 index 00000000..13c4faad --- /dev/null +++ b/internal/controller/reconciler/infra_routes.go @@ -0,0 +1,462 @@ +package reconciler + +import ( + "context" + "fmt" + "strings" + + gkeGatewayApiNetworkingv1 "github.com/GoogleCloudPlatform/gke-gateway-api/apis/networking/v1" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/logx" + "github.com/wandb/operator/pkg/utils" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + v1 "k8s.io/api/core/v1" + apiErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "knative.dev/pkg/ptr" + controllerruntime "sigs.k8s.io/controller-runtime" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +// infraRouteEntry holds the resolved routing info for a single infra component instance. +type infraRouteEntry struct { + // name is the HTTPRoute resource name + name string + namespace string + serviceName string + servicePort gatewayv1.PortNumber + healthCheckPath string + healthCheckPort int32 + ingress *serverManifest.AppIngressSpec +} + +const infraHTTPRouteComponent = "infra-route" + +// resolveInfraRoutes returns one entry per infra instance that has an Ingress spec and +// whose component is enabled in the CR. +func resolveInfraRoutes(ctx context.Context, c ctrlClient.Client, wandb *apiv2.WeightsAndBiases, manifest serverManifest.Manifest) ([]infraRouteEntry, error) { + var entries []infraRouteEntry + + for crKey, instance := range wandb.Spec.ObjectStore { + objectStoreSpec := instance.ManagedObjectStore + if objectStoreSpec == nil { + continue + } + for _, instanceName := range sortedInfraConfigNames(manifest.Bucket) { + cfg := manifest.Bucket[instanceName] + if cfg.Ingress == nil { + continue + } + svcName := fmt.Sprintf("%s-s3", objectStoreSpec.Name) + port, err := resolveInfraServicePort(ctx, c, types.NamespacedName{Name: svcName, Namespace: objectStoreSpec.Namespace}, cfg.Ingress, 8333) + if err != nil { + return nil, fmt.Errorf("bucket instance %q: %w", instanceName, err) + } + entries = append(entries, infraRouteEntry{ + name: fmt.Sprintf("%s-bucket-%s", wandb.Name, infraRouteInstanceName(crKey, instanceName)), + namespace: objectStoreSpec.Namespace, + serviceName: svcName, + servicePort: port, + ingress: cfg.Ingress, + healthCheckPath: "/ready", + healthCheckPort: 4444, + }) + } + } + + for crKey, instance := range wandb.Spec.ClickHouse { + chSpec := instance.ManagedClickHouse + if chSpec == nil { + continue + } + for _, instanceName := range sortedInfraConfigNames(manifest.Clickhouse) { + cfg := manifest.Clickhouse[instanceName] + if cfg.Ingress == nil { + continue + } + clusterName := clickhouseClusterName(chSpec.Name) + svcName := fmt.Sprintf("clickhouse-%s", clusterName) + port, err := resolveInfraServicePort( + ctx, + c, + types.NamespacedName{Name: svcName, Namespace: chSpec.Namespace}, + cfg.Ingress, + 8123, + ) + if err != nil { + return nil, fmt.Errorf("clickhouse instance %q: %w", instanceName, err) + } + entries = append(entries, infraRouteEntry{ + name: fmt.Sprintf("%s-clickhouse-%s", wandb.Name, infraRouteInstanceName(crKey, instanceName)), + namespace: chSpec.Namespace, + serviceName: svcName, + servicePort: port, + ingress: cfg.Ingress, + healthCheckPath: "/ready", + healthCheckPort: port, + }) + } + } + + return entries, nil +} + +// infraRouteInstanceName composes a unique route suffix from the CR instance key +// and the manifest infra-config name. The default CR instance keeps the +// historical suffix (manifest name only) to preserve existing route names. +func infraRouteInstanceName(crKey, manifestInstanceName string) string { + if crKey == apiv2.DefaultInstanceName { + return manifestInstanceName + } + return fmt.Sprintf("%s-%s", crKey, manifestInstanceName) +} + +func resolveInfraServicePort(ctx context.Context, c ctrlClient.Client, serviceRef types.NamespacedName, ingress *serverManifest.AppIngressSpec, defaultPort int32) (gatewayv1.PortNumber, error) { + if ingress != nil && ingress.ServicePort != "" { + parsed := intstr.Parse(ingress.ServicePort) + if parsed.Type != intstr.Int { + service := &v1.Service{} + err := c.Get(ctx, serviceRef, service) + if err != nil { + return 0, err + } + for _, port := range service.Spec.Ports { + // TODO(dpanzella): we need a way to unbind this from the manifest, this works, but is maybe undesirable + if strings.Contains(port.Name, "s3-") { + return port.Port, nil + } + } + return 0, fmt.Errorf("port %s, not found in service %s", parsed.StrVal, serviceRef.Name) + } + return parsed.IntVal, nil + } + return defaultPort, nil +} + +// clickhouseClusterName mirrors the logic in the altinity NsNameBuilder. +func clickhouseClusterName(specName string) string { + name := specName + if len(name) > 15 { + name = name[:15] + } + name = strings.TrimRight(name, "-") + return name +} + +func reconcileInfraHTTPRoutes( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, + manifest serverManifest.Manifest, +) error { + logger := logx.GetSlog(ctx) + if wandb.Spec.Networking.Mode != apiv2.NetworkingModeGatewayAPI { + return nil + } + if wandb.Status.GatewayStatus == nil || wandb.Status.GatewayStatus.GatewayRef == nil { + return nil + } + + gwConfig := wandb.Spec.Networking.GatewayAPI + ref := wandb.Status.GatewayStatus.GatewayRef + + hostname := parseHostname(wandb.Spec.Wandb.Hostname) + hostnames := []gatewayv1.Hostname{gatewayv1.Hostname(hostname)} + for _, h := range wandb.Spec.Wandb.AdditionalHostnames { + hostnames = append(hostnames, gatewayv1.Hostname(h)) + } + + entries, err := resolveInfraRoutes(ctx, c, wandb, manifest) + if err != nil { + return fmt.Errorf("failed to resolve infra routes: %w", err) + } + + desiredNames := make(map[string]bool, len(entries)) + for _, entry := range entries { + desiredNames[infraHTTPRouteKey(entry.namespace, entry.name).String()] = true + + parentRef := buildInfraGatewayParentRef(ref, gwConfig, entry.namespace) + route := buildInfraHTTPRoute(wandb, parentRef, hostnames, entry) + + httpRoute := &gatewayv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: entry.name, + Namespace: entry.namespace, + }, + } + op, err := controllerruntime.CreateOrUpdate(ctx, c, httpRoute, func() error { + httpRoute.Labels = utils.MergeMapsStringString(httpRoute.Labels, route.Labels) + httpRoute.Annotations = utils.MergeMapsStringString(httpRoute.Annotations, route.Annotations) + httpRoute.Spec.ParentRefs = route.Spec.ParentRefs + httpRoute.Spec.Hostnames = route.Spec.Hostnames + httpRoute.Spec.Rules = route.Spec.Rules + if err := setInfraHTTPRouteOwnership(wandb, route, c.Scheme()); err != nil { + return fmt.Errorf("failed to set ownership on infra HTTPRoute %s: %w", entry.name, err) + } + return nil + }) + if err != nil { + return err + } + logger.Info(fmt.Sprintf("Successfully %s HTTPRoute", op), "HTTPRoute", httpRoute.Name) + + if len(httpRoute.Status.Parents) > 0 && httpRoute.Status.Parents[0].ControllerName == "networking.gke.io/gateway" { + healthCheckPolicy := &gkeGatewayApiNetworkingv1.HealthCheckPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: entry.name, + Namespace: entry.namespace, + }, + } + op, err = controllerruntime.CreateOrUpdate(ctx, c, healthCheckPolicy, func() error { + healthCheckPolicy.Labels = utils.MergeMapsStringString(healthCheckPolicy.Labels, infraHealthCheckPolicyLabels(wandb)) + healthCheckPolicy.Spec.Default = &gkeGatewayApiNetworkingv1.HealthCheckPolicyConfig{ + CheckIntervalSec: ptr.Int64(5), + TimeoutSec: ptr.Int64(5), + UnhealthyThreshold: ptr.Int64(3), + HealthyThreshold: ptr.Int64(1), + Config: &gkeGatewayApiNetworkingv1.HealthCheck{ + Type: gkeGatewayApiNetworkingv1.HTTP, + HTTP: &gkeGatewayApiNetworkingv1.HTTPHealthCheck{ + CommonHealthCheck: gkeGatewayApiNetworkingv1.CommonHealthCheck{ + Port: ptr.Int64(int64(entry.healthCheckPort)), + }, + CommonHTTPHealthCheck: gkeGatewayApiNetworkingv1.CommonHTTPHealthCheck{ + RequestPath: ptr.String(entry.healthCheckPath), + }, + }, + }, + } + healthCheckPolicy.Spec.TargetRef = gatewayv1alpha2.NamespacedPolicyTargetReference{ + Kind: "Service", + Name: gatewayv1alpha2.ObjectName(entry.serviceName), + } + if err != nil { + return err + } + if err := controllerutil.SetControllerReference(wandb, healthCheckPolicy, c.Scheme()); err != nil { + return err + } + return nil + }) + if err != nil { + logger.Error("Failed to create or update health check policy", "HealthCheckPolicy", healthCheckPolicy.Name, logx.ErrAttr(err)) + return err + } + logger.Info(fmt.Sprintf("Successfully %s HealthCheckPolicy", op), "HealthCheckPolicy", healthCheckPolicy.Name) + } + } + + if err := deleteStaleInfraHTTPRoutes(ctx, c, wandb, desiredNames); err != nil { + return err + } + + if err := deleteStaleInfraHealthCheckPolicies(ctx, c, wandb, desiredNames); err != nil { + return err + } + + return nil +} + +func buildInfraHTTPRoute( + wandb *apiv2.WeightsAndBiases, + parentRef gatewayv1.ParentReference, + hostnames []gatewayv1.Hostname, + entry infraRouteEntry, +) *gatewayv1.HTTPRoute { + paths := []string{"/"} + if entry.ingress != nil && len(entry.ingress.Paths) > 0 { + paths = entry.ingress.Paths + } + + var matches []gatewayv1.HTTPRouteMatch + for _, p := range paths { + p := p + matchType := gatewayv1.PathMatchPathPrefix + if entry.ingress != nil && entry.ingress.PathType == "Exact" { + matchType = gatewayv1.PathMatchExact + } + matches = append(matches, gatewayv1.HTTPRouteMatch{ + Path: &gatewayv1.HTTPPathMatch{ + Type: &matchType, + Value: &p, + }, + }) + } + + backendRef := gatewayv1.HTTPBackendRef{ + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Name: gatewayv1.ObjectName(entry.serviceName), + Port: &entry.servicePort, + }, + }, + } + + return &gatewayv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: entry.name, + Namespace: entry.namespace, + Labels: infraHTTPRouteLabels(wandb), + }, + Spec: gatewayv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayv1.CommonRouteSpec{ + ParentRefs: []gatewayv1.ParentReference{parentRef}, + }, + Hostnames: hostnames, + Rules: []gatewayv1.HTTPRouteRule{{ + Matches: matches, + BackendRefs: []gatewayv1.HTTPBackendRef{backendRef}, + }}, + }, + } +} + +func buildInfraGatewayParentRef( + ref *apiv2.GatewayReference, + gwConfig *apiv2.GatewayAPIConfig, + routeNamespace string, +) gatewayv1.ParentReference { + parentRef := gatewayv1.ParentReference{ + Name: gatewayv1.ObjectName(ref.Name), + } + if ref.Namespace != "" && ref.Namespace != routeNamespace { + ns := gatewayv1.Namespace(ref.Namespace) + parentRef.Namespace = &ns + } + if gwConfig != nil && gwConfig.ListenerName != nil { + sectionName := gatewayv1.SectionName(*gwConfig.ListenerName) + parentRef.SectionName = §ionName + } + return parentRef +} + +func deleteInfraHTTPRoutes( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, +) error { + return deleteStaleInfraHTTPRoutes(ctx, c, wandb, map[string]bool{}) +} + +// isInfraHTTPRouteName returns true for HTTPRoute names that follow the infra route naming convention. +func isInfraHTTPRouteName(routeName, wandbName string) bool { + infraTypes := []string{"bucket", "clickhouse", "mysql", "redis"} + for _, t := range infraTypes { + prefix := fmt.Sprintf("%s-%s-", wandbName, t) + if strings.HasPrefix(routeName, prefix) { + return true + } + } + return false +} + +func deleteStaleInfraHTTPRoutes( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, + desiredRoutes map[string]bool, +) error { + routeList := &gatewayv1.HTTPRouteList{} + if !utils.IsRegistered(c.Scheme(), routeList) { + return nil + } + if err := c.List(ctx, routeList, ctrlClient.MatchingLabels(infraHTTPRouteLabels(wandb))); err != nil { + return fmt.Errorf("failed to list managed infra HTTPRoutes: %w", err) + } + for i := range routeList.Items { + route := &routeList.Items[i] + key := infraHTTPRouteKey(route.Namespace, route.Name).String() + if desiredRoutes[key] { + continue + } + if err := c.Delete(ctx, route); err != nil && !apiErrors.IsNotFound(err) { + return fmt.Errorf("failed to delete stale infra HTTPRoute %s/%s: %w", route.Namespace, route.Name, err) + } + } + + legacyRouteList := &gatewayv1.HTTPRouteList{} + if err := c.List(ctx, legacyRouteList, ctrlClient.InNamespace(wandb.Namespace)); err != nil { + return fmt.Errorf("failed to list legacy infra HTTPRoutes: %w", err) + } + for i := range legacyRouteList.Items { + route := &legacyRouteList.Items[i] + if !isOwnedBy(route, wandb) || !isInfraHTTPRouteName(route.Name, wandb.Name) { + continue + } + key := infraHTTPRouteKey(route.Namespace, route.Name).String() + if desiredRoutes[key] { + continue + } + if err := c.Delete(ctx, route); err != nil && !apiErrors.IsNotFound(err) { + return fmt.Errorf("failed to delete legacy infra HTTPRoute %s/%s: %w", route.Namespace, route.Name, err) + } + } + + return nil +} + +func deleteStaleInfraHealthCheckPolicies( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, + desiredPolicies map[string]bool, +) error { + policyList := &gkeGatewayApiNetworkingv1.HealthCheckPolicyList{} + if !utils.IsRegistered(c.Scheme(), policyList) { + return nil + } + if err := c.List(ctx, policyList, ctrlClient.MatchingLabels(infraHealthCheckPolicyLabels(wandb))); err != nil { + return fmt.Errorf("failed to list managed infra HealthCheckPolicies: %w", err) + } + for i := range policyList.Items { + policy := &policyList.Items[i] + key := infraHTTPRouteKey(policy.Namespace, policy.Name).String() + if desiredPolicies[key] { + continue + } + if err := c.Delete(ctx, policy); err != nil && !apiErrors.IsNotFound(err) { + return fmt.Errorf("failed to delete stale infra HealthCheckPolicy %s/%s: %w", policy.Namespace, policy.Name, err) + } + } + + return nil +} + +func infraHealthCheckPolicyLabels(wandb *apiv2.WeightsAndBiases) map[string]string { + labels := infraHTTPRouteLabels(wandb) + labels[common.WandbComponentLabel] = "infra-healthcheck-policy" + return labels +} + +func infraHTTPRouteLabels(wandb *apiv2.WeightsAndBiases) map[string]string { + return map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + common.WandbNameLabel: wandb.Name, + common.WandbNamespaceLabel: wandb.Namespace, + common.WandbComponentLabel: infraHTTPRouteComponent, + } +} + +func infraHTTPRouteKey(namespace, name string) types.NamespacedName { + return types.NamespacedName{Namespace: namespace, Name: name} +} + +func setInfraHTTPRouteOwnership( + wandb *apiv2.WeightsAndBiases, + route *gatewayv1.HTTPRoute, + scheme *runtime.Scheme, +) error { + if route.Namespace != wandb.Namespace { + route.OwnerReferences = nil + return nil + } + return controllerutil.SetOwnerReference(wandb, route, scheme) +} diff --git a/internal/controller/reconciler/ingress.go b/internal/controller/reconciler/ingress.go new file mode 100644 index 00000000..75410b57 --- /dev/null +++ b/internal/controller/reconciler/ingress.go @@ -0,0 +1,236 @@ +package reconciler + +import ( + "context" + + apiv2 "github.com/wandb/operator/api/v2" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + apiErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// consolidatedIngressName returns spec.networking.ingress.name when set, or +// the default "<cr-name>-ingress" otherwise. +func consolidatedIngressName(wandb *apiv2.WeightsAndBiases) string { + if wandb.Spec.Networking.Ingress != nil && wandb.Spec.Networking.Ingress.Name != "" { + return wandb.Spec.Networking.Ingress.Name + } + return wandb.Name +} + +func reconcileConsolidatedIngress(ctx context.Context, c ctrlClient.Client, wandb *apiv2.WeightsAndBiases, manifest serverManifest.Manifest) error { + ingressName := consolidatedIngressName(wandb) + hostname := parseHostname(wandb.Spec.Wandb.Hostname) + + var paths []networkingv1.HTTPIngressPath + + for _, app := range sortedManifestApplications(manifest) { + if len(app.Features) > 0 && !manifest.FeaturesEnabled(app.Features) { + continue + } + if app.Ingress == nil || app.Service == nil { + continue + } + + appPaths := []string{"/"} + pathType := networkingv1.PathTypePrefix + if app.Ingress != nil { + if len(app.Ingress.Paths) > 0 { + appPaths = app.Ingress.Paths + } + switch app.Ingress.PathType { + case "Exact": + pathType = networkingv1.PathTypeExact + case "ImplementationSpecific": + pathType = networkingv1.PathTypeImplementationSpecific + } + } + + serviceName := app.Name + servicePort := resolveIngressServicePort(app) + + for _, p := range appPaths { + paths = append(paths, networkingv1.HTTPIngressPath{ + Path: p, + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: serviceName, + Port: servicePort, + }, + }, + }) + } + } + + infraRoutes, err := resolveInfraRoutes(ctx, c, wandb, manifest) + if err != nil { + return err + } + for _, route := range infraRoutes { + if len(route.ingress.Paths) == 0 { + continue + } + pathType := networkingv1.PathType(route.ingress.PathType) + paths = append(paths, networkingv1.HTTPIngressPath{ + Path: route.ingress.Paths[0], + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: route.serviceName, + Port: networkingv1.ServiceBackendPort{ + Number: route.servicePort, + }, + }, + }, + }) + } + + if len(paths) == 0 { + return nil + } + + rules := []networkingv1.IngressRule{{ + Host: hostname, + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{Paths: paths}, + }, + }} + + for _, additionalHost := range wandb.Spec.Wandb.AdditionalHostnames { + rules = append(rules, networkingv1.IngressRule{ + Host: additionalHost, + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{Paths: paths}, + }, + }) + } + + annotations := map[string]string{} + for k, v := range wandb.Spec.Networking.Annotations { + annotations[k] = v + } + + if wandb.Spec.Networking.TLS != nil && wandb.Spec.Networking.TLS.CertManager != nil { + cm := wandb.Spec.Networking.TLS.CertManager + if cm.ClusterIssuer != "" { + annotations["cert-manager.io/cluster-issuer"] = cm.ClusterIssuer + } + if cm.Issuer != "" { + annotations["cert-manager.io/issuer"] = cm.Issuer + } + } + + desired := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: ingressName, + Namespace: wandb.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + }, + Annotations: annotations, + }, + Spec: networkingv1.IngressSpec{ + Rules: rules, + }, + } + + if wandb.Spec.Networking.Ingress != nil { + desired.Spec.IngressClassName = wandb.Spec.Networking.Ingress.IngressClassName + if *desired.Spec.IngressClassName == "nginx" { + desired.Annotations["nginx.ingress.kubernetes.io/proxy-body-size"] = "0" + } + } + + if wandb.Spec.Networking.TLS != nil && wandb.Spec.Networking.TLS.SecretName != "" { + allHosts := []string{hostname} + allHosts = append(allHosts, wandb.Spec.Wandb.AdditionalHostnames...) + desired.Spec.TLS = []networkingv1.IngressTLS{{ + Hosts: allHosts, + SecretName: wandb.Spec.Networking.TLS.SecretName, + }} + } + + if err := controllerutil.SetOwnerReference(wandb, desired, c.Scheme()); err != nil { + return err + } + + current := &networkingv1.Ingress{} + err = c.Get(ctx, types.NamespacedName{Name: ingressName, Namespace: wandb.Namespace}, current) + if err != nil { + if apiErrors.IsNotFound(err) { + if err := c.Create(ctx, desired); err != nil { + return err + } + wandb.Status.IngressStatus = summarizeIngressStatus(desired) + return nil + } + return err + } + + desired.ResourceVersion = current.ResourceVersion + if err := c.Update(ctx, desired); err != nil { + return err + } + wandb.Status.IngressStatus = summarizeIngressStatus(current) + return nil +} + +func deleteConsolidatedIngress(ctx context.Context, c ctrlClient.Client, wandb *apiv2.WeightsAndBiases) error { + ingressName := consolidatedIngressName(wandb) + ingress := &networkingv1.Ingress{} + if err := c.Get(ctx, types.NamespacedName{Name: ingressName, Namespace: wandb.Namespace}, ingress); err != nil { + if apiErrors.IsNotFound(err) { + return nil + } + return err + } + return c.Delete(ctx, ingress) +} + +func resolveIngressServicePort(app serverManifest.Application) networkingv1.ServiceBackendPort { + if app.Ingress != nil && app.Ingress.ServicePort != "" { + port := intstr.Parse(app.Ingress.ServicePort) + if port.Type == intstr.Int { + return networkingv1.ServiceBackendPort{Number: port.IntVal} + } + return networkingv1.ServiceBackendPort{Name: port.StrVal} + } + if app.Service != nil && len(app.Service.Ports) > 0 { + return networkingv1.ServiceBackendPort{Number: app.Service.Ports[0].Port} + } + return networkingv1.ServiceBackendPort{Number: 8080} +} + +func summarizeIngressStatus(ingress *networkingv1.Ingress) *apiv2.IngressStatusSummary { + if ingress == nil { + return nil + } + + summary := &apiv2.IngressStatusSummary{ + Name: ingress.Name, + } + for _, lb := range ingress.Status.LoadBalancer.Ingress { + loadBalancerIngress := corev1.LoadBalancerIngress{ + IP: lb.IP, + Hostname: lb.Hostname, + } + for _, port := range lb.Ports { + loadBalancerIngress.Ports = append(loadBalancerIngress.Ports, corev1.PortStatus{ + Port: port.Port, + Protocol: port.Protocol, + Error: port.Error, + }) + } + summary.LoadBalancerIngress = append(summary.LoadBalancerIngress, loadBalancerIngress) + } + + return summary +} diff --git a/internal/controller/reconciler/ingress_test.go b/internal/controller/reconciler/ingress_test.go new file mode 100644 index 00000000..35ed0e0c --- /dev/null +++ b/internal/controller/reconciler/ingress_test.go @@ -0,0 +1,51 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +*/ + +package reconciler + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + apiv2 "github.com/wandb/operator/api/v2" +) + +func TestConsolidatedIngressName_DefaultsToCRName(t *testing.T) { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb"}, + } + require.Equal(t, "wandb", consolidatedIngressName(wandb)) +} + +func TestConsolidatedIngressName_HonorsSpecOverride(t *testing.T) { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb"}, + Spec: apiv2.WeightsAndBiasesSpec{ + Networking: apiv2.NetworkingSpec{ + Ingress: &apiv2.IngressConfig{Name: "custom-name"}, + }, + }, + } + require.Equal(t, "custom-name", consolidatedIngressName(wandb)) +} + +func TestConsolidatedIngressName_EmptyOverrideFallsBack(t *testing.T) { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb"}, + Spec: apiv2.WeightsAndBiasesSpec{ + Networking: apiv2.NetworkingSpec{ + Ingress: &apiv2.IngressConfig{Name: ""}, + }, + }, + } + require.Equal(t, "wandb", consolidatedIngressName(wandb)) +} diff --git a/internal/controller/reconciler/kafka.go b/internal/controller/reconciler/kafka.go new file mode 100644 index 00000000..e99b9d6e --- /dev/null +++ b/internal/controller/reconciler/kafka.go @@ -0,0 +1,267 @@ +package reconciler + +import ( + "context" + "fmt" + "time" + + "github.com/twmb/franz-go/pkg/kadm" + "github.com/twmb/franz-go/pkg/kerr" + "github.com/twmb/franz-go/pkg/kgo" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/managed/kafka/bufstream" + "github.com/wandb/operator/internal/logx" + "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func kafkaWriteState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + mfst manifest.Manifest, +) []metav1.Condition { + if wandb.Spec.Kafka.ManagedKafka != nil { + return managedKafkaWriteState(ctx, client, wandb, mfst) + } + return nil +} + +func kafkaReadState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + newConditions []metav1.Condition, +) ([]metav1.Condition, *apiv2.KafkaConnection) { + if wandb.Spec.Kafka.ManagedKafka != nil { + return managedKafkaReadState(ctx, client, wandb, newConditions) + } + return newConditions, nil +} + +func kafkaInferStatus( + ctx context.Context, + client client.Client, + recorder record.EventRecorder, + wandb *apiv2.WeightsAndBiases, + newConditions []metav1.Condition, + newInfraConn *apiv2.KafkaConnection, +) (ctrl.Result, error) { + if wandb.Spec.Kafka.ManagedKafka != nil { + return managedKafkaInferStatus(ctx, client, recorder, wandb, newConditions, newInfraConn) + } + return ctrl.Result{}, nil +} + +func kafkaPurgeFinalizer( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, +) error { + if spec := wandb.Spec.Kafka.ManagedKafka; spec != nil { + specNamespacedName := managedKafkaSpecNamespacedName(spec) + onDeleteRule := bufstream.ToKafkaOnDeleteRule(wandb, wandb.GetRetentionPolicy(spec.ManagedInfraSpec)) + return bufstream.PurgeFinalizer(ctx, client, specNamespacedName, onDeleteRule) + } + return nil +} + +func kafkaDetachFinalizer( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, +) error { + spec := wandb.Spec.Kafka.ManagedKafka + if spec == nil { + return nil + } + specNamespacedName := managedKafkaSpecNamespacedName(spec) + return bufstream.DetachFinalizer(ctx, client, specNamespacedName, wandb) +} + +// managed + +func managedKafkaWriteState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + mfst manifest.Manifest, +) []metav1.Condition { + spec := wandb.Spec.Kafka.ManagedKafka + specNamespacedName := managedKafkaSpecNamespacedName(spec) + + if conditions := bufstream.CheckDetached(ctx, client, specNamespacedName, wandb.GetUID(), spec.Replicas); conditions != nil { + return conditions + } + + return bufstream.WriteState(ctx, client, wandb, mfst) +} + +func managedKafkaReadState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + newConditions []metav1.Condition, +) ([]metav1.Condition, *apiv2.KafkaConnection) { + spec := wandb.Spec.Kafka.ManagedKafka + + specNamespacedName := managedKafkaSpecNamespacedName(spec) + onDeleteRule := bufstream.ToKafkaOnDeleteRule(wandb, wandb.GetRetentionPolicy(spec.ManagedInfraSpec)) + readConditions, newInfraConn := bufstream.ReadState(ctx, client, specNamespacedName, wandb, onDeleteRule) + newConditions = append(newConditions, readConditions...) + return newConditions, newInfraConn +} + +func managedKafkaInferStatus( + ctx context.Context, + client client.Client, + recorder record.EventRecorder, + wandb *apiv2.WeightsAndBiases, + newConditions []metav1.Condition, + newInfraConn *apiv2.KafkaConnection, +) (ctrl.Result, error) { + statusBefore := wandb.DeepCopy().Status + oldConditions := wandb.Status.KafkaStatus.Conditions + oldInfraConn := wandb.Status.KafkaStatus.Connection + + enabled := true + updatedStatus, events, ctrlResult := bufstream.ComputeStatus( + ctx, + enabled, + oldConditions, + newConditions, + utils.Coalesce(newInfraConn, &oldInfraConn), + wandb.Generation, + ) + for _, e := range events { + recorder.Event(wandb, e.Type, e.Reason, e.Message) + } + wandb.Status.KafkaStatus = updatedStatus + err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore) + + return ctrlResult, err +} + +// helpers + +func managedKafkaSpecNamespacedName(spec *apiv2.ManagedKafkaSpec) types.NamespacedName { + return types.NamespacedName{ + Namespace: spec.Namespace, + Name: spec.Name, + } +} + +// createKafkaTopics provisions the manifest-defined topics directly via the Kafka +// Admin API. Bufstream is Kafka-protocol compatible, so topic creation is +// idempotent: an already-existing topic is treated as success. +func createKafkaTopics(ctx context.Context, cl client.Client, wandb *apiv2.WeightsAndBiases, manifest manifest.Manifest) (ctrl.Result, error) { + if wandb.Spec.Kafka.ManagedKafka == nil { + return ctrl.Result{}, nil + } + log := logx.GetSlog(ctx) + + bootstrap, err := resolveKafkaBootstrap(ctx, cl, wandb) + if err != nil { + log.Error("failed to resolve kafka bootstrap endpoint", logx.ErrAttr(err)) + return ctrl.Result{RequeueAfter: defaultRequeueDuration}, nil + } + + kafkaSpec := wandb.Spec.Kafka.ManagedKafka + replicationFactor := int16(1) + if kafkaSpec.Config.ReplicationConfig.DefaultReplicationFactor > 0 { + replicationFactor = int16(kafkaSpec.Config.ReplicationConfig.DefaultReplicationFactor) + } + + adminClient, err := kgo.NewClient( + kgo.SeedBrokers(bootstrap), + kgo.ClientID("wandb-operator"), + ) + if err != nil { + log.Error("failed to create kafka client", logx.ErrAttr(err)) + return ctrl.Result{RequeueAfter: defaultRequeueDuration}, nil + } + defer adminClient.Close() + + admin := kadm.NewClient(adminClient) + + dialCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + for _, topic := range manifest.Kafka.Topics { + if len(topic.Features) > 0 && !manifest.FeaturesEnabled(topic.Features) { + continue + } + + topicName := topic.Topic + if topicName == "" { + topicName = topic.Name + } + if topicName == "" { + continue + } + + partitions := int32(1) + if topic.PartitionCount > 0 { + partitions = int32(topic.PartitionCount) + } + + if err := createTopicIdempotent(dialCtx, admin, topicName, partitions, replicationFactor); err != nil { + log.Error("failed to create kafka topic", logx.ErrAttr(err), "topic", topicName) + return ctrl.Result{RequeueAfter: defaultRequeueDuration}, nil + } + log.Debug("ensured kafka topic", "topic", topicName, "partitions", partitions) + } + + return ctrl.Result{}, nil +} + +func createTopicIdempotent(ctx context.Context, admin *kadm.Client, topicName string, partitions int32, replicationFactor int16) error { + resp, err := admin.CreateTopics(ctx, partitions, replicationFactor, nil, topicName) + if err != nil { + return err + } + for _, ct := range resp { + if ct.Err != nil && ct.Err != kerr.TopicAlreadyExists { + return fmt.Errorf("create topic %q: %w", ct.Topic, ct.Err) + } + } + return nil +} + +// resolveKafkaBootstrap reads the managed Kafka connection secret to obtain the +// in-cluster broker host:port used by the admin client. +func resolveKafkaBootstrap(ctx context.Context, cl client.Client, wandb *apiv2.WeightsAndBiases) (string, error) { + conn := wandb.Status.KafkaStatus.Connection + secretName := conn.Host.Name + if secretName == "" { + return "", fmt.Errorf("kafka connection secret not set in status") + } + + spec := wandb.Spec.Kafka.ManagedKafka + secret := &corev1.Secret{} + found, err := common.GetResource( + ctx, cl, + types.NamespacedName{Namespace: spec.Namespace, Name: secretName}, + "Secret", secret, + ) + if err != nil { + return "", err + } + if !found { + return "", fmt.Errorf("kafka connection secret %s not found", secretName) + } + + host := string(secret.Data["Host"]) + port := string(secret.Data["Port"]) + if host == "" || port == "" { + return "", fmt.Errorf("kafka connection secret missing host/port") + } + return fmt.Sprintf("%s:%s", host, port), nil +} diff --git a/internal/controller/reconciler/legacy_overrides.go b/internal/controller/reconciler/legacy_overrides.go new file mode 100644 index 00000000..e6bb73a8 --- /dev/null +++ b/internal/controller/reconciler/legacy_overrides.go @@ -0,0 +1,77 @@ +package reconciler + +import ( + "context" + "sort" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/logx" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" +) + +// validateLegacyOverrides logs legacyOverrides keys that are neither "global" +// nor a manifest application. The spec is left untouched — unknown keys are +// simply never applied. +func validateLegacyOverrides(ctx context.Context, wandb *apiv2.WeightsAndBiases, manifest serverManifest.Manifest) { + if len(wandb.Spec.Wandb.LegacyOverrides) == 0 { + return + } + logger := logx.GetSlog(ctx) + + keys := make([]string, 0, len(wandb.Spec.Wandb.LegacyOverrides)) + for key := range wandb.Spec.Wandb.LegacyOverrides { + keys = append(keys, key) + } + sort.Strings(keys) + + for _, key := range keys { + if key == apiv2.LegacyOverridesGlobalKey { + continue + } + if _, ok := manifest.Applications[key]; ok { + continue + } + logger.Warn("legacy override section does not map to any application in the server manifest; ignoring", + "section", key, "version", wandb.Spec.Wandb.Version) + } +} + +// applyLegacyOverrideEnv layers global then per-app overrides (per-app wins) +// onto a fully built env list, so they beat manifest and injected vars — as +// in v1, where user env displaced chart-computed env. +func applyLegacyOverrideEnv(ctx context.Context, wandb *apiv2.WeightsAndBiases, appName string, envVars []corev1.EnvVar) []corev1.EnvVar { + overrides := wandb.Spec.Wandb.LegacyOverrides + if len(overrides) == 0 { + return envVars + } + envVars = overrideEnvVars(ctx, envVars, overrides[apiv2.LegacyOverridesGlobalKey].Env) + envVars = overrideEnvVars(ctx, envVars, overrides[appName].Env) + return envVars +} + +// overrideEnvVars replaces same-named vars in place and appends the rest — +// the inverse of appendMissingEnvVars. Empty names skip with a log; a later +// duplicate wins. +func overrideEnvVars(ctx context.Context, base []corev1.EnvVar, overrides []corev1.EnvVar) []corev1.EnvVar { + if len(overrides) == 0 { + return base + } + index := make(map[string]int, len(base)) + for i, envVar := range base { + index[envVar.Name] = i + } + for _, envVar := range overrides { + if envVar.Name == "" { + logx.GetSlog(ctx).Warn("skipping legacy override env var with empty name") + continue + } + if i, ok := index[envVar.Name]; ok { + base[i] = envVar + continue + } + index[envVar.Name] = len(base) + base = append(base, envVar) + } + return base +} diff --git a/internal/controller/reconciler/legacy_overrides_test.go b/internal/controller/reconciler/legacy_overrides_test.go new file mode 100644 index 00000000..d518d757 --- /dev/null +++ b/internal/controller/reconciler/legacy_overrides_test.go @@ -0,0 +1,181 @@ +package reconciler + +import ( + "context" + "testing" + + apiv2 "github.com/wandb/operator/api/v2" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" +) + +func envNamesValues(envVars []corev1.EnvVar) map[string]string { + out := make(map[string]string, len(envVars)) + for _, envVar := range envVars { + out[envVar.Name] = envVar.Value + } + return out +} + +func TestOverrideEnvVarsReplacesInPlaceAndAppends(t *testing.T) { + base := []corev1.EnvVar{ + {Name: "A", Value: "base-a"}, + {Name: "B", Value: "base-b"}, + } + result := overrideEnvVars(context.Background(), base, []corev1.EnvVar{ + {Name: "B", Value: "override-b"}, + {Name: "C", Value: "override-c"}, + }) + + if len(result) != 3 { + t.Fatalf("expected 3 env vars, got %d: %v", len(result), result) + } + // Replaced in place: order preserved for existing names. + if result[1].Name != "B" || result[1].Value != "override-b" { + t.Errorf("expected B replaced in place, got %v", result[1]) + } + if result[2].Name != "C" || result[2].Value != "override-c" { + t.Errorf("expected C appended, got %v", result[2]) + } + if result[0].Value != "base-a" { + t.Errorf("expected A untouched, got %v", result[0]) + } +} + +func TestOverrideEnvVarsReplacesValueFrom(t *testing.T) { + base := []corev1.EnvVar{ + {Name: "SECRET", ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "old"}, + Key: "k", + }, + }}, + } + result := overrideEnvVars(context.Background(), base, []corev1.EnvVar{ + {Name: "SECRET", Value: "literal-now"}, + }) + + if result[0].ValueFrom != nil || result[0].Value != "literal-now" { + t.Errorf("expected override to fully replace the var, got %+v", result[0]) + } +} + +func TestOverrideEnvVarsHandAuthoredEdgeCases(t *testing.T) { + base := []corev1.EnvVar{{Name: "A", Value: "base-a"}} + result := overrideEnvVars(context.Background(), base, []corev1.EnvVar{ + {Name: "", Value: "skipped"}, + {Name: "DUP", Value: "first"}, + {Name: "DUP", Value: "last-wins"}, + }) + + values := envNamesValues(result) + if _, ok := values[""]; ok { + t.Error("empty-name entry should be skipped") + } + if values["DUP"] != "last-wins" { + t.Errorf("expected later duplicate to win, got %q", values["DUP"]) + } + if len(result) != 2 { + t.Fatalf("expected 2 env vars, got %d: %v", len(result), result) + } +} + +func TestOverrideEnvVarsNoOverridesNoOp(t *testing.T) { + base := []corev1.EnvVar{{Name: "A", Value: "base-a"}} + result := overrideEnvVars(context.Background(), base, nil) + if len(result) != 1 || result[0].Value != "base-a" { + t.Errorf("expected base unchanged, got %v", result) + } +} + +func TestApplyLegacyOverrideEnvPrecedence(t *testing.T) { + wandb := testWeightsAndBiases() + wandb.Spec.Wandb.LegacyOverrides = map[string]apiv2.LegacyOverrides{ + apiv2.LegacyOverridesGlobalKey: {Env: []corev1.EnvVar{ + {Name: "GLOBAL_ONLY", Value: "global"}, + {Name: "BOTH", Value: "from-global"}, + {Name: "MANIFEST_VAR", Value: "global-override"}, + }}, + "api": {Env: []corev1.EnvVar{ + {Name: "BOTH", Value: "from-app"}, + {Name: "APP_ONLY", Value: "app"}, + }}, + } + + base := []corev1.EnvVar{{Name: "MANIFEST_VAR", Value: "manifest"}} + + result := applyLegacyOverrideEnv(context.Background(), wandb, "api", base) + values := envNamesValues(result) + + if values["MANIFEST_VAR"] != "global-override" { + t.Errorf("expected override to beat manifest env, got %q", values["MANIFEST_VAR"]) + } + if values["BOTH"] != "from-app" { + t.Errorf("expected per-app to beat global, got %q", values["BOTH"]) + } + if values["GLOBAL_ONLY"] != "global" || values["APP_ONLY"] != "app" { + t.Errorf("expected both layers present, got %v", values) + } +} + +func TestApplyLegacyOverrideEnvAppWithoutEntryGetsGlobal(t *testing.T) { + wandb := testWeightsAndBiases() + wandb.Spec.Wandb.LegacyOverrides = map[string]apiv2.LegacyOverrides{ + apiv2.LegacyOverridesGlobalKey: {Env: []corev1.EnvVar{{Name: "HTTP_PROXY", Value: "http://proxy"}}}, + "parquet": {Env: []corev1.EnvVar{{Name: "PARQUET_VAR", Value: "x"}}}, + } + + result := applyLegacyOverrideEnv(context.Background(), wandb, "weave", nil) + values := envNamesValues(result) + + if values["HTTP_PROXY"] != "http://proxy" { + t.Errorf("expected global env applied, got %v", values) + } + if _, ok := values["PARQUET_VAR"]; ok { + t.Error("another app's overrides must not apply") + } +} + +func TestApplyLegacyOverrideEnvNoOverrides(t *testing.T) { + base := []corev1.EnvVar{{Name: "A", Value: "a"}} + result := applyLegacyOverrideEnv(context.Background(), testWeightsAndBiases(), "api", base) + if len(result) != 1 || result[0].Value != "a" { + t.Errorf("expected base unchanged, got %v", result) + } +} + +func TestValidateLegacyOverridesDoesNotMutateSpec(t *testing.T) { + wandb := testWeightsAndBiases() + wandb.Spec.Wandb.LegacyOverrides = map[string]apiv2.LegacyOverrides{ + apiv2.LegacyOverridesGlobalKey: {Env: []corev1.EnvVar{{Name: "A", Value: "1"}}}, + "api": {Env: []corev1.EnvVar{{Name: "B", Value: "2"}}}, + "console": {Env: []corev1.EnvVar{{Name: "C", Value: "3"}}}, + "app": {Env: []corev1.EnvVar{{Name: "D", Value: "4"}}}, + } + manifest := serverManifest.Manifest{ + Applications: map[string]serverManifest.Application{ + "api": {Name: "api"}, + }, + } + + validateLegacyOverrides(context.Background(), wandb, manifest) + + // Unmapped keys (console, app) are logged but must remain in the spec. + if len(wandb.Spec.Wandb.LegacyOverrides) != 4 { + t.Fatalf("expected spec untouched, got %v", wandb.Spec.Wandb.LegacyOverrides) + } + for _, key := range []string{apiv2.LegacyOverridesGlobalKey, "api", "console", "app"} { + if _, ok := wandb.Spec.Wandb.LegacyOverrides[key]; !ok { + t.Errorf("expected key %q to remain in spec", key) + } + } +} + +func TestValidateLegacyOverridesEmptyInputs(t *testing.T) { + // Must not panic with nil overrides or an empty manifest. + validateLegacyOverrides(context.Background(), testWeightsAndBiases(), serverManifest.Manifest{}) + + wandb := testWeightsAndBiases() + wandb.Spec.Wandb.LegacyOverrides = map[string]apiv2.LegacyOverrides{"api": {}} + validateLegacyOverrides(context.Background(), wandb, serverManifest.Manifest{}) +} diff --git a/internal/controller/reconciler/manifest_order.go b/internal/controller/reconciler/manifest_order.go new file mode 100644 index 00000000..2f7865f6 --- /dev/null +++ b/internal/controller/reconciler/manifest_order.go @@ -0,0 +1,25 @@ +package reconciler + +import ( + "maps" + "slices" + + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" +) + +func sortedManifestApplications(manifest serverManifest.Manifest) []serverManifest.Application { + appNames := slices.Sorted(maps.Keys(manifest.Applications)) + apps := make([]serverManifest.Application, 0, len(appNames)) + for _, appName := range appNames { + app := manifest.Applications[appName] + if app.Name == "" { + app.Name = appName + } + apps = append(apps, app) + } + return apps +} + +func sortedInfraConfigNames(configs map[string]serverManifest.InfraConfig) []string { + return slices.Sorted(maps.Keys(configs)) +} diff --git a/internal/controller/reconciler/migrate_legacy.go b/internal/controller/reconciler/migrate_legacy.go new file mode 100644 index 00000000..1fc33318 --- /dev/null +++ b/internal/controller/reconciler/migrate_legacy.go @@ -0,0 +1,434 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strconv" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + + apiv1 "github.com/wandb/operator/api/v1" + apiv2 "github.com/wandb/operator/api/v2" + externalobjectstore "github.com/wandb/operator/internal/controller/infra/external/objectstore" +) + +// migrateLegacyAnnotations drains `legacy.operator.wandb.com/*-pending` +// annotations into materialized Secrets and typed spec references. Returns +// a Requeue result when any change was applied. +func migrateLegacyAnnotations( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, +) (ctrl.Result, error) { + mysqlChanged, err := migrateLegacyMySQL(ctx, c, wandb) + if err != nil { + return ctrl.Result{}, err + } + redisChanged, err := migrateLegacyRedis(ctx, c, wandb) + if err != nil { + return ctrl.Result{}, err + } + bucketChanged, err := migrateLegacyBucket(ctx, c, wandb) + if err != nil { + return ctrl.Result{}, err + } + oidcChanged, err := migrateLegacyOIDC(ctx, c, wandb) + if err != nil { + return ctrl.Result{}, err + } + if !mysqlChanged && !redisChanged && !bucketChanged && !oidcChanged { + return ctrl.Result{}, nil + } + + if err := c.Update(ctx, wandb); err != nil { + return ctrl.Result{}, fmt.Errorf("update CR after legacy migration: %w", err) + } + return ctrl.Result{RequeueAfter: time.Second}, nil +} + +// legacyMySQLPayload is the literal-string subset the webhook couldn't turn +// into typed selectors. Port is `any` to accept JSON number or string. +type legacyMySQLPayload struct { + Host string `json:"host,omitempty"` + Port any `json:"port,omitempty"` + Database string `json:"database,omitempty"` + User string `json:"user,omitempty"` + Password string `json:"password,omitempty"` + CaCert string `json:"caCert,omitempty"` +} + +// migrateLegacyMySQL drains the mysql-pending annotation into a Secret + +// externalMysql selectors. Fields the webhook already set are preserved; +// only zero selectors are filled. Returns (changed, err). +func migrateLegacyMySQL( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, +) (bool, error) { + raw, ok := wandb.Annotations[apiv1.MySQLPendingAnnotation] + if !ok { + return false, nil + } + + dec := json.NewDecoder(strings.NewReader(raw)) + dec.UseNumber() + var payload legacyMySQLPayload + if err := dec.Decode(&payload); err != nil { + return false, fmt.Errorf("decode %s: %w", apiv1.MySQLPendingAnnotation, err) + } + + secretName := fmt.Sprintf("%s-mysql-converted", wandb.Name) + conn := wandb.Spec.MySQL[apiv2.DefaultInstanceName].ExternalMysql + if conn == nil { + conn = &apiv2.MysqlConnection{} + } + + data := map[string][]byte{} + fill := func(target *corev1.SecretKeySelector, dataKey, value string) { + if target.Name != "" || value == "" { + return + } + data[dataKey] = []byte(value) + *target = secretSelector(secretName, dataKey) + } + + fill(&conn.Host, "host", payload.Host) + fill(&conn.Port, "port", normalizePort(payload.Port)) + fill(&conn.Database, "database", payload.Database) + fill(&conn.Username, "username", payload.User) + fill(&conn.SslCa, "sslCa", payload.CaCert) + fill(&conn.Password, "password", payload.Password) + + if err := materializeConvertedSecret(ctx, c, wandb, secretName, data); err != nil { + return false, err + } + + setExternalInstance(&wandb.Spec.MySQL, func(s *apiv2.MySQLSpec) { s.ExternalMysql = conn }) + delete(wandb.Annotations, apiv1.MySQLPendingAnnotation) + return true, nil +} + +// legacyRedisPayload is the literal-string subset the webhook couldn't turn +// into typed selectors. +type legacyRedisPayload struct { + Host string `json:"host,omitempty"` + Port any `json:"port,omitempty"` + Password string `json:"password,omitempty"` + CaCert string `json:"caCert,omitempty"` + Tls string `json:"tls,omitempty"` +} + +// migrateLegacyRedis drains the redis-pending annotation. Mirrors +// migrateLegacyMySQL — only zero externalRedis selectors are filled. +func migrateLegacyRedis( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, +) (bool, error) { + raw, ok := wandb.Annotations[apiv1.RedisPendingAnnotation] + if !ok { + return false, nil + } + + dec := json.NewDecoder(strings.NewReader(raw)) + dec.UseNumber() + var payload legacyRedisPayload + if err := dec.Decode(&payload); err != nil { + return false, fmt.Errorf("decode %s: %w", apiv1.RedisPendingAnnotation, err) + } + + secretName := fmt.Sprintf("%s-redis-converted", wandb.Name) + conn := wandb.Spec.Redis[apiv2.DefaultInstanceName].ExternalRedis + if conn == nil { + conn = &apiv2.RedisConnection{} + } + + data := map[string][]byte{} + fill := func(target *corev1.SecretKeySelector, dataKey, value string) { + if target.Name != "" || value == "" { + return + } + data[dataKey] = []byte(value) + *target = secretSelector(secretName, dataKey) + } + + fill(&conn.Host, "host", payload.Host) + fill(&conn.Port, "port", normalizePort(payload.Port)) + fill(&conn.Password, "password", payload.Password) + fill(&conn.SslCa, "sslCa", payload.CaCert) + fill(&conn.Tls, "tls", payload.Tls) + + if err := materializeConvertedSecret(ctx, c, wandb, secretName, data); err != nil { + return false, err + } + + setExternalInstance(&wandb.Spec.Redis, func(s *apiv2.RedisSpec) { s.ExternalRedis = conn }) + delete(wandb.Annotations, apiv1.RedisPendingAnnotation) + return true, nil +} + +// legacyBucketPayload is the flat literal subset from the webhook's +// bucket+defaultBucket merge. kmsKey has no v2 home; ignored. +type legacyBucketPayload struct { + Provider string `json:"provider,omitempty"` + Name string `json:"name,omitempty"` + Path string `json:"path,omitempty"` + Region string `json:"region,omitempty"` + AccessKey string `json:"accessKey,omitempty"` + SecretKey string `json:"secretKey,omitempty"` +} + +// migrateLegacyBucket drains the bucket-pending annotation. Webhook-set +// fields (typically AccessKey/SecretKey) are preserved. +func migrateLegacyBucket( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, +) (bool, error) { + raw, ok := wandb.Annotations[apiv1.BucketPendingAnnotation] + if !ok { + return false, nil + } + + var payload legacyBucketPayload + if err := json.Unmarshal([]byte(raw), &payload); err != nil { + return false, fmt.Errorf("decode %s: %w", apiv1.BucketPendingAnnotation, err) + } + + secretName := fmt.Sprintf("%s-bucket-converted", wandb.Name) + conn := wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ExternalObjectStore + if conn == nil { + conn = &apiv2.ObjectStoreConnection{} + } + + name, path, query := splitBucketQuery(payload.Name, payload.Path) + endpoint, port, bucket := parseBucketName(name) + // Query param beats the region field, matching gorilla's precedence. + region := payload.Region + if v := query.Get("region"); v != "" { + region = v + } + forcePathStyle, tlsEnabled := deriveBucketAddressing(payload.Provider, endpoint, query) + + data := map[string][]byte{} + fill := func(target *corev1.SecretKeySelector, dataKey, value string) { + if target.Name != "" || value == "" { + return + } + data[dataKey] = []byte(value) + *target = secretSelector(secretName, dataKey) + } + + fill(&conn.Endpoint, "endpoint", endpoint) + fill(&conn.Port, "port", port) + fill(&conn.Bucket, "bucket", bucket) + fill(&conn.Path, "path", strings.Trim(path, "/")) + fill(&conn.Region, "region", region) + fill(&conn.AccessKey, "accessKey", payload.AccessKey) + fill(&conn.SecretKey, "secretKey", payload.SecretKey) + fill(&conn.ForcePathStyle, "forcePathStyle", forcePathStyle) + fill(&conn.TlsEnabled, "tlsEnabled", tlsEnabled) + + if err := materializeConvertedSecret(ctx, c, wandb, secretName, data); err != nil { + return false, err + } + + setExternalInstance(&wandb.Spec.ObjectStore, func(s *apiv2.ObjectStoreSpec) { s.ExternalObjectStore = conn }) + delete(wandb.Annotations, apiv1.BucketPendingAnnotation) + return true, nil +} + +// setExternalInstance applies fn to the default instance of an infra map, +// creating the map and/or default entry when absent. +func setExternalInstance[T any](m *map[string]T, fn func(*T)) { + if *m == nil { + *m = map[string]T{} + } + instance := (*m)[apiv2.DefaultInstanceName] + fn(&instance) + (*m)[apiv2.DefaultInstanceName] = instance +} + +// splitBucketQuery strips the ?tls=/?forcePathStyle=/?region= overrides gorilla +// accepted on v1 bucket URLs; they rode in bucket.name or bucket.path. +func splitBucketQuery(name, path string) (cleanName, cleanPath string, q url.Values) { + raw := "" + if i := strings.IndexByte(path, '?'); i >= 0 { + path, raw = path[:i], path[i+1:] + } + if i := strings.IndexByte(name, '?'); i >= 0 { + name, raw = name[:i], name[i+1:] + } + query, err := url.ParseQuery(raw) + if err != nil { + return name, path, url.Values{} + } + return name, path, query +} + +// deriveBucketAddressing decides forcePathStyle/tlsEnabled for a drained v1 bucket: +// explicit ?forcePathStyle=/?tls= win, else any embedded endpoint means path-style over +// http (prefixes belong in bucket.path, so a host in bucket.name is always an endpoint). +func deriveBucketAddressing(provider, endpoint string, query url.Values) (forcePathStyle, tlsEnabled string) { + if provider != "" && provider != "s3" && provider != "cw" { + return "", "" + } + fps := provider != "cw" && externalobjectstore.RequiresPathStyle(endpoint) + if v, err := strconv.ParseBool(query.Get("forcePathStyle")); err == nil { + fps = v + } + forcePathStyle = strconv.FormatBool(fps) + if endpoint == "" { + return forcePathStyle, "" + } + // gorilla defaulted S3-compatible endpoints to http, CoreWeave to https. + tls := provider == "cw" + if v, err := strconv.ParseBool(query.Get("tls")); err == nil { + tls = v + } + return forcePathStyle, strconv.FormatBool(tls) +} + +// parseBucketName splits v1's bucket.name. A "/" indicates the embedded +// "host[:port]/bucket" form (S3 bucket names can't contain "/"); otherwise +// the whole string is the bucket name. +func parseBucketName(name string) (endpoint, port, bucket string) { + if name == "" || !strings.Contains(name, "/") { + return "", "", name + } + slash := strings.IndexByte(name, '/') + host := name[:slash] + bucket = name[slash+1:] + if colon := strings.IndexByte(host, ':'); colon >= 0 { + return host[:colon], host[colon+1:], bucket + } + return host, "", bucket +} + +// legacyOIDCPayload is the literal-string subset the webhook couldn't turn +// into typed selectors. +type legacyOIDCPayload struct { + ClientId string `json:"clientId,omitempty"` + Secret string `json:"secret,omitempty"` + AuthMethod string `json:"authMethod,omitempty"` + Issuer string `json:"issuer,omitempty"` +} + +// migrateLegacyOIDC drains the oidc-pending annotation. Only zero +// spec.wandb.oidc selectors are filled. +func migrateLegacyOIDC( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, +) (bool, error) { + raw, ok := wandb.Annotations[apiv1.OIDCPendingAnnotation] + if !ok { + return false, nil + } + + var payload legacyOIDCPayload + if err := json.Unmarshal([]byte(raw), &payload); err != nil { + return false, fmt.Errorf("decode %s: %w", apiv1.OIDCPendingAnnotation, err) + } + + secretName := fmt.Sprintf("%s-oidc-converted", wandb.Name) + oidc := &wandb.Spec.Wandb.OIDC + + data := map[string][]byte{} + fill := func(target *corev1.SecretKeySelector, dataKey, value string) { + if target.Name != "" || value == "" { + return + } + data[dataKey] = []byte(value) + *target = secretSelector(secretName, dataKey) + } + + fill(&oidc.ClientId, "clientId", payload.ClientId) + fill(&oidc.ClientSecret, "clientSecret", payload.Secret) + fill(&oidc.AuthMethod, "authMethod", payload.AuthMethod) + fill(&oidc.IssuerUrl, "issuerUrl", payload.Issuer) + + if err := materializeConvertedSecret(ctx, c, wandb, secretName, data); err != nil { + return false, err + } + + delete(wandb.Annotations, apiv1.OIDCPendingAnnotation) + return true, nil +} + +// materializeConvertedSecret CreateOrUpdates an opaque Secret with data, +// no-op when empty. Safe to call on partial-migration retries. +func materializeConvertedSecret( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, + secretName string, + data map[string][]byte, +) error { + if len(data) == 0 { + return nil + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: wandb.Namespace, + }, + } + if _, err := ctrl.CreateOrUpdate(ctx, c, secret, func() error { + secret.Type = corev1.SecretTypeOpaque + secret.Data = data + return nil + }); err != nil { + return fmt.Errorf("create or update %s: %w", secretName, err) + } + return nil +} + +func normalizePort(v any) string { + switch p := v.(type) { + case nil: + return "" + case string: + return p + case json.Number: + if i, err := p.Int64(); err == nil { + if i == 0 { + return "" + } + return strconv.FormatInt(i, 10) + } + return string(p) + default: + return fmt.Sprintf("%v", p) + } +} + +func secretSelector(name, key string) corev1.SecretKeySelector { + return corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: key, + } +} diff --git a/internal/controller/reconciler/migrate_legacy_test.go b/internal/controller/reconciler/migrate_legacy_test.go new file mode 100644 index 00000000..76ff18a2 --- /dev/null +++ b/internal/controller/reconciler/migrate_legacy_test.go @@ -0,0 +1,719 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + apiErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + apiv1 "github.com/wandb/operator/api/v1" + apiv2 "github.com/wandb/operator/api/v2" +) + +func newMigrationFixture( + t *testing.T, + annotations map[string]string, + mutate func(*apiv2.WeightsAndBiases), + seed ...ctrlClient.Object, +) (ctrlClient.Client, *apiv2.WeightsAndBiases) { + t.Helper() + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + require.NoError(t, apiv2.AddToScheme(scheme)) + + wandb := &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{APIVersion: "apps.wandb.com/v2", Kind: "WeightsAndBiases"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "default", + Annotations: annotations, + }, + } + if mutate != nil { + mutate(wandb) + } + objects := append([]ctrlClient.Object{wandb}, seed...) + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() + return client, wandb +} + +func getConvertedSecret(t *testing.T, c ctrlClient.Client) (*corev1.Secret, error) { + t.Helper() + var secret corev1.Secret + err := c.Get(context.Background(), types.NamespacedName{Name: "wandb-mysql-converted", Namespace: "default"}, &secret) + return &secret, err +} + +func getRedisConvertedSecret(t *testing.T, c ctrlClient.Client) (*corev1.Secret, error) { + t.Helper() + var secret corev1.Secret + err := c.Get(context.Background(), types.NamespacedName{Name: "wandb-redis-converted", Namespace: "default"}, &secret) + return &secret, err +} + +func getBucketConvertedSecret(t *testing.T, c ctrlClient.Client) (*corev1.Secret, error) { + t.Helper() + var secret corev1.Secret + err := c.Get(context.Background(), types.NamespacedName{Name: "wandb-bucket-converted", Namespace: "default"}, &secret) + return &secret, err +} + +func getOIDCConvertedSecret(t *testing.T, c ctrlClient.Client) (*corev1.Secret, error) { + t.Helper() + var secret corev1.Secret + err := c.Get(context.Background(), types.NamespacedName{Name: "wandb-oidc-converted", Namespace: "default"}, &secret) + return &secret, err +} + +func TestMigrateLegacyAnnotations_NoAnnotation(t *testing.T) { + client, wandb := newMigrationFixture(t, nil, nil) + res, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + require.Zero(t, res.RequeueAfter) + require.Nil(t, wandb.Spec.MySQL[apiv2.DefaultInstanceName].ExternalMysql) + + _, err = getConvertedSecret(t, client) + require.True(t, apiErrors.IsNotFound(err), "expected no converted Secret, got err=%v", err) +} + +func TestMigrateLegacyMySQL_FullLiteralPayload(t *testing.T) { + payload := `{"host":"mysql.example.com","port":3306,"database":"wandb_local","user":"wandb","password":"shh","caCert":"---cert---"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.MySQLPendingAnnotation: payload, + }, nil) + + res, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + require.NotZero(t, res.RequeueAfter) + + secret, err := getConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, corev1.SecretTypeOpaque, secret.Type) + require.Equal(t, []byte("mysql.example.com"), secret.Data["host"]) + require.Equal(t, []byte("3306"), secret.Data["port"]) + require.Equal(t, []byte("wandb_local"), secret.Data["database"]) + require.Equal(t, []byte("wandb"), secret.Data["username"]) + require.Equal(t, []byte("shh"), secret.Data["password"]) + require.Equal(t, []byte("---cert---"), secret.Data["sslCa"]) + + var fresh apiv2.WeightsAndBiases + require.NoError(t, client.Get(context.Background(), types.NamespacedName{Name: "wandb", Namespace: "default"}, &fresh)) + require.NotContains(t, fresh.Annotations, apiv1.MySQLPendingAnnotation) + require.NotNil(t, fresh.Spec.MySQL[apiv2.DefaultInstanceName].ExternalMysql) + conn := fresh.Spec.MySQL[apiv2.DefaultInstanceName].ExternalMysql + require.Equal(t, "wandb-mysql-converted", conn.Host.Name) + require.Equal(t, "host", conn.Host.Key) + require.Equal(t, "port", conn.Port.Key) + require.Equal(t, "database", conn.Database.Key) + require.Equal(t, "username", conn.Username.Key) + require.Equal(t, "password", conn.Password.Key) + require.Equal(t, "sslCa", conn.SslCa.Key) + require.Empty(t, conn.Tls.Name) + require.Empty(t, conn.SslCert.Name) + require.Empty(t, conn.SslKey.Name) + require.Empty(t, conn.URL.Name) +} + +func TestMigrateLegacyMySQL_PartialPayload(t *testing.T) { + payload := `{"host":"mysql.example.com","password":"shh"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.MySQLPendingAnnotation: payload, + }, nil) + + res, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + require.NotZero(t, res.RequeueAfter) + + secret, err := getConvertedSecret(t, client) + require.NoError(t, err) + require.Contains(t, secret.Data, "host") + require.Contains(t, secret.Data, "password") + require.NotContains(t, secret.Data, "port") + require.NotContains(t, secret.Data, "database") + require.NotContains(t, secret.Data, "username") + require.NotContains(t, secret.Data, "sslCa") + + conn := wandb.Spec.MySQL[apiv2.DefaultInstanceName].ExternalMysql + require.NotNil(t, conn) + require.Equal(t, "host", conn.Host.Key) + require.Equal(t, "password", conn.Password.Key) + require.Empty(t, conn.Port.Name) + require.Empty(t, conn.Database.Name) + require.Empty(t, conn.Username.Name) + require.Empty(t, conn.SslCa.Name) +} + +func TestMigrateLegacyMySQL_PreSetFieldsAreRespected(t *testing.T) { + payload := `{"host":"mysql.example.com","port":3306,"database":"wandb_local"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.MySQLPendingAnnotation: payload, + }, func(w *apiv2.WeightsAndBiases) { + w.Spec.MySQL = map[string]apiv2.MySQLSpec{ + apiv2.DefaultInstanceName: { + ExternalMysql: &apiv2.MysqlConnection{ + Host: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "preset-secret"}, + Key: "preset-host-key", + }, + }, + }, + } + }) + + res, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + require.NotZero(t, res.RequeueAfter) + + secret, err := getConvertedSecret(t, client) + require.NoError(t, err) + require.NotContains(t, secret.Data, "host", "host was already set by the webhook; reconciler must not overwrite") + require.Contains(t, secret.Data, "port") + require.Contains(t, secret.Data, "database") + + conn := wandb.Spec.MySQL[apiv2.DefaultInstanceName].ExternalMysql + require.Equal(t, "preset-secret", conn.Host.Name) + require.Equal(t, "preset-host-key", conn.Host.Key) + require.Equal(t, "wandb-mysql-converted", conn.Port.Name) + require.Equal(t, "wandb-mysql-converted", conn.Database.Name) +} + +func TestMigrateLegacyMySQL_AllPreSetEmptyAnnotationPayload(t *testing.T) { + payload := `{}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.MySQLPendingAnnotation: payload, + }, func(w *apiv2.WeightsAndBiases) { + w.Spec.MySQL = map[string]apiv2.MySQLSpec{ + apiv2.DefaultInstanceName: { + ExternalMysql: &apiv2.MysqlConnection{ + Host: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "preset"}, + Key: "host", + }, + }, + }, + } + }) + + res, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + require.NotZero(t, res.RequeueAfter, "annotation removal alone still constitutes a change") + + _, err = getConvertedSecret(t, client) + require.True(t, apiErrors.IsNotFound(err), "no literal data, so no converted Secret should be created") + + var fresh apiv2.WeightsAndBiases + require.NoError(t, client.Get(context.Background(), types.NamespacedName{Name: "wandb", Namespace: "default"}, &fresh)) + require.NotContains(t, fresh.Annotations, apiv1.MySQLPendingAnnotation) + require.Equal(t, "preset", fresh.Spec.MySQL[apiv2.DefaultInstanceName].ExternalMysql.Host.Name) +} + +func TestMigrateLegacyMySQL_PreExistingSecretOverwritten(t *testing.T) { + stale := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb-mysql-converted", Namespace: "default"}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "host": []byte("OLD"), + "obsolete": []byte("OLD"), + }, + } + payload := `{"host":"mysql.example.com","password":"shh"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.MySQLPendingAnnotation: payload, + }, nil, stale) + + res, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + require.NotZero(t, res.RequeueAfter) + + secret, err := getConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("mysql.example.com"), secret.Data["host"]) + require.NotContains(t, secret.Data, "obsolete") +} + +func TestMigrateLegacyMySQL_MalformedJSON(t *testing.T) { + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.MySQLPendingAnnotation: "{not json", + }, nil) + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.Error(t, err) + + require.Contains(t, wandb.Annotations, apiv1.MySQLPendingAnnotation) + require.Nil(t, wandb.Spec.MySQL[apiv2.DefaultInstanceName].ExternalMysql) +} + +func TestMigrateLegacyMySQL_EmptyAnnotation(t *testing.T) { + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.MySQLPendingAnnotation: "", + }, nil) + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.Error(t, err) +} + +func TestMigrateLegacyMySQL_PortStringified(t *testing.T) { + payload := `{"host":"mysql.example.com","port":3307}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.MySQLPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("3307"), secret.Data["port"]) +} + +func TestMigrateLegacyRedis_FullLiteralPayload(t *testing.T) { + payload := `{"host":"redis.example.com","port":6379,"password":"shh","caCert":"---cert---","tls":"true"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.RedisPendingAnnotation: payload, + }, nil) + + res, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + require.NotZero(t, res.RequeueAfter) + + secret, err := getRedisConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, corev1.SecretTypeOpaque, secret.Type) + require.Equal(t, []byte("redis.example.com"), secret.Data["host"]) + require.Equal(t, []byte("6379"), secret.Data["port"]) + require.Equal(t, []byte("shh"), secret.Data["password"]) + require.Equal(t, []byte("---cert---"), secret.Data["sslCa"]) + require.Equal(t, []byte("true"), secret.Data["tls"]) + + var fresh apiv2.WeightsAndBiases + require.NoError(t, client.Get(context.Background(), types.NamespacedName{Name: "wandb", Namespace: "default"}, &fresh)) + require.NotContains(t, fresh.Annotations, apiv1.RedisPendingAnnotation) + conn := fresh.Spec.Redis[apiv2.DefaultInstanceName].ExternalRedis + require.NotNil(t, conn) + require.Equal(t, "wandb-redis-converted", conn.Host.Name) + require.Equal(t, "host", conn.Host.Key) + require.Equal(t, "port", conn.Port.Key) + require.Equal(t, "password", conn.Password.Key) + require.Equal(t, "sslCa", conn.SslCa.Key) + require.Equal(t, "tls", conn.Tls.Key) + require.Empty(t, conn.URL.Name) +} + +func TestMigrateLegacyRedis_PreSetFieldsAreRespected(t *testing.T) { + payload := `{"host":"redis.example.com","port":6379}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.RedisPendingAnnotation: payload, + }, func(w *apiv2.WeightsAndBiases) { + w.Spec.Redis = map[string]apiv2.RedisSpec{ + apiv2.DefaultInstanceName: { + ExternalRedis: &apiv2.RedisConnection{ + Host: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "preset-secret"}, + Key: "preset-host-key", + }, + }, + }, + } + }) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getRedisConvertedSecret(t, client) + require.NoError(t, err) + require.NotContains(t, secret.Data, "host") + require.Contains(t, secret.Data, "port") + + conn := wandb.Spec.Redis[apiv2.DefaultInstanceName].ExternalRedis + require.Equal(t, "preset-secret", conn.Host.Name) + require.Equal(t, "preset-host-key", conn.Host.Key) + require.Equal(t, "wandb-redis-converted", conn.Port.Name) +} + +func TestMigrateLegacyRedis_MalformedJSON(t *testing.T) { + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.RedisPendingAnnotation: "{not json", + }, nil) + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.Error(t, err) + + require.Contains(t, wandb.Annotations, apiv1.RedisPendingAnnotation) + require.Nil(t, wandb.Spec.Redis[apiv2.DefaultInstanceName].ExternalRedis) +} + +func TestMigrateLegacyAnnotations_MySQLAndRedisInOneCall(t *testing.T) { + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.MySQLPendingAnnotation: `{"host":"mysql.example.com"}`, + apiv1.RedisPendingAnnotation: `{"host":"redis.example.com"}`, + }, nil) + + res, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + require.NotZero(t, res.RequeueAfter) + + var fresh apiv2.WeightsAndBiases + require.NoError(t, client.Get(context.Background(), types.NamespacedName{Name: "wandb", Namespace: "default"}, &fresh)) + require.NotContains(t, fresh.Annotations, apiv1.MySQLPendingAnnotation) + require.NotContains(t, fresh.Annotations, apiv1.RedisPendingAnnotation) + require.NotNil(t, fresh.Spec.MySQL[apiv2.DefaultInstanceName].ExternalMysql) + require.NotNil(t, fresh.Spec.Redis[apiv2.DefaultInstanceName].ExternalRedis) + + mysqlSecret, err := getConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("mysql.example.com"), mysqlSecret.Data["host"]) + + redisSecret, err := getRedisConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("redis.example.com"), redisSecret.Data["host"]) +} + +func TestMigrateLegacyMySQL_PortStringValueAccepted(t *testing.T) { + payload := `{"host":"mysql.example.com","port":"3308"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.MySQLPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("3308"), secret.Data["port"]) +} + +func TestParseBucketName(t *testing.T) { + cases := []struct { + name string + endpoint, port, bkt string + }{ + {"", "", "", ""}, + {"my-bucket", "", "", "my-bucket"}, + {"minio.example.com/wandb", "minio.example.com", "", "wandb"}, + {"minio.example.com:9000/wandb", "minio.example.com", "9000", "wandb"}, + {"minio:9000/wandb", "minio", "9000", "wandb"}, + {"minio.minio.svc.cluster.local:9000/bucket", "minio.minio.svc.cluster.local", "9000", "bucket"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e, p, b := parseBucketName(tc.name) + require.Equal(t, tc.endpoint, e) + require.Equal(t, tc.port, p) + require.Equal(t, tc.bkt, b) + }) + } +} + +func TestMigrateLegacyBucket_BareBucketName(t *testing.T) { + payload := `{"name":"my-bucket","region":"us-east-1","accessKey":"AKIA","secretKey":"shh"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + res, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + require.NotZero(t, res.RequeueAfter) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("my-bucket"), secret.Data["bucket"]) + require.Equal(t, []byte("us-east-1"), secret.Data["region"]) + require.Equal(t, []byte("AKIA"), secret.Data["accessKey"]) + require.Equal(t, []byte("shh"), secret.Data["secretKey"]) + require.NotContains(t, secret.Data, "endpoint") + require.NotContains(t, secret.Data, "port") + require.Equal(t, []byte("false"), secret.Data["forcePathStyle"], "bare name means native AWS, virtual-hosted") + require.NotContains(t, secret.Data, "tlsEnabled", "tls is meaningless without a custom endpoint") + + conn := wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ExternalObjectStore + require.NotNil(t, conn) + require.Equal(t, "bucket", conn.Bucket.Key) + require.Equal(t, "region", conn.Region.Key) + require.Equal(t, "accessKey", conn.AccessKey.Key) + require.Equal(t, "secretKey", conn.SecretKey.Key) + require.Empty(t, conn.Endpoint.Name) + require.Empty(t, conn.Port.Name) + require.Equal(t, "forcePathStyle", conn.ForcePathStyle.Key) +} + +func TestMigrateLegacyBucket_EmbeddedEndpoint(t *testing.T) { + payload := `{"name":"minio.minio.svc:9000/wandb-bucket"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("minio.minio.svc"), secret.Data["endpoint"]) + require.Equal(t, []byte("9000"), secret.Data["port"]) + require.Equal(t, []byte("wandb-bucket"), secret.Data["bucket"]) + require.Equal(t, []byte("true"), secret.Data["forcePathStyle"], "custom endpoint requires path-style") + require.Equal(t, []byte("false"), secret.Data["tlsEnabled"], "gorilla defaulted custom endpoints to http") + + conn := wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ExternalObjectStore + require.Equal(t, "endpoint", conn.Endpoint.Key) + require.Equal(t, "port", conn.Port.Key) + require.Equal(t, "bucket", conn.Bucket.Key) + require.Equal(t, "forcePathStyle", conn.ForcePathStyle.Key) + require.Equal(t, "tlsEnabled", conn.TlsEnabled.Key) +} + +func TestMigrateLegacyBucket_QueryParamOverrides(t *testing.T) { + payload := `{"provider":"s3","name":"minio.example.com:9000/wandb","region":"us-east-1","path":"prefix?tls=true&forcePathStyle=false®ion=eu-west-1"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("minio.example.com"), secret.Data["endpoint"]) + require.Equal(t, []byte("wandb"), secret.Data["bucket"]) + require.Equal(t, []byte("false"), secret.Data["forcePathStyle"], "explicit ?forcePathStyle= wins over the endpoint rule") + require.Equal(t, []byte("true"), secret.Data["tlsEnabled"], "explicit ?tls= wins over the http default") + require.Equal(t, []byte("eu-west-1"), secret.Data["region"], "?region= beats the region field, matching gorilla") + require.Equal(t, []byte("prefix"), secret.Data["path"], "the prefix survives with its query stripped") +} + +func TestMigrateLegacyBucket_PathPrefix(t *testing.T) { + payload := `{"name":"minio.example.com:9000/wandb","path":"wandb-files/"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("wandb-files"), secret.Data["path"]) + + conn := wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ExternalObjectStore + require.Equal(t, "path", conn.Path.Key) +} + +func TestMigrateLegacyBucket_QueryOnlyPath(t *testing.T) { + payload := `{"name":"my-bucket","path":"?forcePathStyle=true"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.NotContains(t, secret.Data, "path", "a query-only path carries no prefix") + require.Equal(t, []byte("true"), secret.Data["forcePathStyle"], "explicit override applies even without an endpoint") +} + +func TestMigrateLegacyBucket_QueryInName(t *testing.T) { + payload := `{"name":"minio.example.com/wandb?tls=true"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("wandb"), secret.Data["bucket"], "query must be stripped from the bucket name") + require.Equal(t, []byte("true"), secret.Data["forcePathStyle"]) + require.Equal(t, []byte("true"), secret.Data["tlsEnabled"]) +} + +func TestMigrateLegacyBucket_AwsEndpointInName(t *testing.T) { + payload := `{"name":"s3.us-east-1.amazonaws.com/my-bucket"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("s3.us-east-1.amazonaws.com"), secret.Data["endpoint"]) + require.Equal(t, []byte("true"), secret.Data["forcePathStyle"], + "a host in bucket.name is always an endpoint (prefixes belong in bucket.path) and gets path-style") +} + +func TestMigrateLegacyBucket_CoreWeaveProvider(t *testing.T) { + payload := `{"provider":"cw","name":"cwobject.com/my-bucket"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("false"), secret.Data["forcePathStyle"], "CoreWeave object storage is virtual-hosted") + require.Equal(t, []byte("true"), secret.Data["tlsEnabled"], "CoreWeave object storage is https") +} + +func TestMigrateLegacyBucket_NonS3ProviderSkipsAddressing(t *testing.T) { + payload := `{"provider":"gcs","name":"my-gcs-bucket"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.NotContains(t, secret.Data, "forcePathStyle") + require.NotContains(t, secret.Data, "tlsEnabled") +} + +func TestMigrateLegacyBucket_PreSetCredentialsRespected(t *testing.T) { + payload := `{"name":"my-bucket","accessKey":"FROM_ANNOTATION","secretKey":"FROM_ANNOTATION"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, func(w *apiv2.WeightsAndBiases) { + w.Spec.ObjectStore = map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: { + ExternalObjectStore: &apiv2.ObjectStoreConnection{ + AccessKey: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "preset"}, + Key: "ACCESS_KEY", + }, + SecretKey: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "preset"}, + Key: "SECRET_KEY", + }, + }, + }, + } + }) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.NotContains(t, secret.Data, "accessKey", "webhook-set AccessKey must not be overwritten") + require.NotContains(t, secret.Data, "secretKey", "webhook-set SecretKey must not be overwritten") + require.Contains(t, secret.Data, "bucket") + + conn := wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ExternalObjectStore + require.Equal(t, "preset", conn.AccessKey.Name) + require.Equal(t, "preset", conn.SecretKey.Name) + require.Equal(t, "wandb-bucket-converted", conn.Bucket.Name) +} + +func TestMigrateLegacyBucket_UnknownFieldsIgnored(t *testing.T) { + payload := `{"name":"my-bucket","provider":"s3","path":"sub/path","kmsKey":"arn:aws:kms:..."}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err, "unknown fields should be tolerated and dropped") + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.NotContains(t, secret.Data, "provider") + require.Equal(t, []byte("sub/path"), secret.Data["path"]) + require.NotContains(t, secret.Data, "kmsKey") +} + +func TestMigrateLegacyBucket_MalformedJSON(t *testing.T) { + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: "{not json", + }, nil) + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.Error(t, err) + require.Contains(t, wandb.Annotations, apiv1.BucketPendingAnnotation) + require.Nil(t, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ExternalObjectStore) +} + +func TestMigrateLegacyOIDC_AllLiterals(t *testing.T) { + payload := `{"clientId":"abc","secret":"shh","authMethod":"client_secret_post","issuer":"https://idp.example.com"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.OIDCPendingAnnotation: payload, + }, nil) + + res, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + require.NotZero(t, res.RequeueAfter) + + secret, err := getOIDCConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("abc"), secret.Data["clientId"]) + require.Equal(t, []byte("shh"), secret.Data["clientSecret"]) + require.Equal(t, []byte("client_secret_post"), secret.Data["authMethod"]) + require.Equal(t, []byte("https://idp.example.com"), secret.Data["issuerUrl"]) + + oidc := wandb.Spec.Wandb.OIDC + require.Equal(t, "wandb-oidc-converted", oidc.ClientId.Name) + require.Equal(t, "clientId", oidc.ClientId.Key) + require.Equal(t, "clientSecret", oidc.ClientSecret.Key) + require.Equal(t, "authMethod", oidc.AuthMethod.Key) + require.Equal(t, "issuerUrl", oidc.IssuerUrl.Key) + + var fresh apiv2.WeightsAndBiases + require.NoError(t, client.Get(context.Background(), types.NamespacedName{Name: "wandb", Namespace: "default"}, &fresh)) + require.NotContains(t, fresh.Annotations, apiv1.OIDCPendingAnnotation) +} + +func TestMigrateLegacyOIDC_PreSetClientSecretRespected(t *testing.T) { + payload := `{"clientId":"abc","secret":"shh"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.OIDCPendingAnnotation: payload, + }, func(w *apiv2.WeightsAndBiases) { + w.Spec.Wandb.OIDC.ClientSecret = corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "preset-oidc"}, + Key: "PRESET", + } + }) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getOIDCConvertedSecret(t, client) + require.NoError(t, err) + require.Contains(t, secret.Data, "clientId") + require.NotContains(t, secret.Data, "clientSecret") + + oidc := wandb.Spec.Wandb.OIDC + require.Equal(t, "preset-oidc", oidc.ClientSecret.Name) + require.Equal(t, "PRESET", oidc.ClientSecret.Key) +} + +func TestMigrateLegacyOIDC_MalformedJSON(t *testing.T) { + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.OIDCPendingAnnotation: "{not json", + }, nil) + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.Error(t, err) + require.Contains(t, wandb.Annotations, apiv1.OIDCPendingAnnotation) +} diff --git a/internal/controller/reconciler/mysql.go b/internal/controller/reconciler/mysql.go new file mode 100644 index 00000000..d5e5faf2 --- /dev/null +++ b/internal/controller/reconciler/mysql.go @@ -0,0 +1,460 @@ +package reconciler + +import ( + "context" + "fmt" + + mocov1beta2 "github.com/cybozu-go/moco/api/v1beta2" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/external" + externalmysql "github.com/wandb/operator/internal/controller/infra/external/mysql" + "github.com/wandb/operator/internal/controller/infra/managed/mysql/moco" + "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/pkg/wandb/manifest" + "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func mysqlWriteState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + mfst manifest.Manifest, +) map[string][]metav1.Condition { + out := map[string][]metav1.Condition{} + for key, spec := range wandb.Spec.MySQL { + switch { + case spec.ManagedMysql != nil: + out[key] = managedMysqlWriteState(ctx, client, wandb, spec.ManagedMysql, mfst) + case spec.ExternalMysql != nil: + out[key] = externalmysql.WriteState(ctx, client, wandb, key, spec.ExternalMysql) + } + } + return out +} + +func mysqlReadState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + conditions map[string][]metav1.Condition, +) (map[string][]metav1.Condition, map[string]*apiv2.MysqlConnection) { + outConds := map[string][]metav1.Condition{} + outConns := map[string]*apiv2.MysqlConnection{} + for key, spec := range wandb.Spec.MySQL { + switch { + case spec.ManagedMysql != nil: + outConds[key], outConns[key] = managedMysqlReadState(ctx, client, wandb, spec.ManagedMysql, conditions[key]) + case spec.ExternalMysql != nil: + outConds[key], outConns[key] = externalmysql.ReadState(ctx, client, wandb, key, conditions[key]) + default: + outConds[key] = conditions[key] + } + } + return outConds, outConns +} + +func mysqlInferStatus( + ctx context.Context, + client client.Client, + recorder record.EventRecorder, + wandb *apiv2.WeightsAndBiases, + conditions map[string][]metav1.Condition, + infraConns map[string]*apiv2.MysqlConnection, +) (ctrl.Result, error) { + if wandb.Status.MySQLStatus == nil { + wandb.Status.MySQLStatus = map[string]apiv2.MysqlInfraStatus{} + } + var results []ctrl.Result + var firstErr error + for key, spec := range wandb.Spec.MySQL { + var res ctrl.Result + var err error + switch { + case spec.ManagedMysql != nil: + res, err = managedMysqlInferStatus(ctx, client, recorder, wandb, key, conditions[key], infraConns[key]) + case spec.ExternalMysql != nil: + res, err = externalMysqlInferStatus(ctx, client, wandb, key, conditions[key], infraConns[key]) + } + results = append(results, res) + if err != nil && firstErr == nil { + firstErr = err + } + } + return consolidateResults(results), firstErr +} + +// runMysqlRetentionFinalizer applies the configured retention policy for a +// single MySQL instance during deletion. +func runMysqlRetentionFinalizer(ctx context.Context, c client.Client, wandb *apiv2.WeightsAndBiases, key string, spec apiv2.MySQLSpec) error { + switch wandb.GetRetentionPolicy(mysqlInstanceInfraSpec(spec)).OnDelete { + case apiv2.PurgeOnDelete: + return mysqlPurgeFinalizer(ctx, c, wandb, key, spec) + case apiv2.DetachOnDelete: + return mysqlDetachFinalizer(ctx, c, wandb, key, spec) + } + return nil +} + +func mysqlInstanceInfraSpec(spec apiv2.MySQLSpec) apiv2.ManagedInfraSpec { + if spec.ManagedMysql != nil { + return spec.ManagedMysql.ManagedInfraSpec + } + return apiv2.ManagedInfraSpec{} +} + +func mysqlPurgeFinalizer( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + key string, + spec apiv2.MySQLSpec, +) error { + if managed := spec.ManagedMysql; managed != nil { + specNamespacedName := managedMysqlSpecNamespacedName(managed) + onDeleteRule := moco.ToMysqlOnDeleteRule(wandb, wandb.GetRetentionPolicy(managed.ManagedInfraSpec)) + return moco.PurgeFinalizer(ctx, client, specNamespacedName, onDeleteRule) + } + if spec.ExternalMysql != nil { + return externalmysql.DeleteConnectionSecret(ctx, client, wandb, key) + } + return nil +} + +func mysqlDetachFinalizer( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + _ string, + spec apiv2.MySQLSpec, +) error { + managed := spec.ManagedMysql + if managed == nil { + return nil + } + specNamespacedName := managedMysqlSpecNamespacedName(managed) + return moco.DetachFinalizer(ctx, client, specNamespacedName, wandb) +} + +// managed + +func managedMysqlWriteState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedMysqlSpec, + mfst manifest.Manifest, +) []metav1.Condition { + var specNamespacedName = managedMysqlSpecNamespacedName(spec) + logger := ctrl.LoggerFrom(ctx) + + dbPasswordSecret := &corev1.Secret{} + err := client.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("%s-%s", specNamespacedName.Name, "db-password"), Namespace: specNamespacedName.Namespace}, dbPasswordSecret) + if err != nil { + if errors.IsNotFound(err) { + dbPasswordSecret.Name = fmt.Sprintf("%s-%s", specNamespacedName.Name, "db-password") + dbPasswordSecret.Namespace = specNamespacedName.Namespace + userPassword, err := utils.GenerateRandomPassword(32) + if err != nil { + logger.Error(err, "failed to generate random password") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ControllerErrorReason, + }, + } + } + rootPassword, err := utils.GenerateRandomPassword(32) + if err != nil { + logger.Error(err, "failed to generate random password") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ControllerErrorReason, + }, + } + } + + dbPasswordSecret.Labels = moco.BuildWandbMysqlLabels(wandb) + dbPasswordSecret.Data = map[string][]byte{ + "rootUser": []byte("root"), + "rootPassword": []byte(rootPassword), + "rootHost": []byte("%"), + "password": []byte(userPassword), + } + if err = client.Create(ctx, dbPasswordSecret); err != nil { + logger.Error(err, "failed to create db password secret") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }, + } + } + } else { + logger.Error(err, "failed to retrieve db password secret") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }, + } + } + } + + if conditions := moco.CheckDetached(ctx, client, specNamespacedName, wandb.GetUID(), spec.Replicas); conditions != nil { + return conditions + } + + var desired *mocov1beta2.MySQLCluster + var confMap *corev1.ConfigMap + desired, confMap, err = moco.ToMocoMySQLClusterSpec(ctx, *spec, wandb, client.Scheme(), mfst) + if err != nil { + logger.Error(err, "failed to translate moco spec") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ControllerErrorReason, + }, + } + } + return moco.WriteState(ctx, client, specNamespacedName, desired, confMap, moco.BuildWandbMysqlLabels(wandb)) +} + +func managedMysqlReadState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedMysqlSpec, + newConditions []metav1.Condition, +) ([]metav1.Condition, *apiv2.MysqlConnection) { + specNamespacedName := managedMysqlSpecNamespacedName(spec) + + readConditions, newInfraConn := moco.ReadState(ctx, client, specNamespacedName, wandb, moco.ToMysqlOnDeleteRule(wandb, wandb.GetRetentionPolicy(spec.ManagedInfraSpec))) + newConditions = append(newConditions, readConditions...) + return newConditions, newInfraConn +} + +func managedMysqlInferStatus( + ctx context.Context, + client client.Client, + recorder record.EventRecorder, + wandb *apiv2.WeightsAndBiases, + key string, + newConditions []metav1.Condition, + newInfraConn *apiv2.MysqlConnection, +) (ctrl.Result, error) { + statusBefore := wandb.DeepCopy().Status + enabled := true + oldStatus := wandb.Status.MySQLStatus[key] + oldConditions := oldStatus.Conditions + oldInfraConn := oldStatus.Connection + + updatedStatus, events, ctrlResult := moco.ComputeStatus( + ctx, + enabled, + oldConditions, + newConditions, + utils.Coalesce(newInfraConn, &oldInfraConn), + wandb.Generation, + ) + + for _, e := range events { + recorder.Event(wandb, e.Type, e.Reason, e.Message) + } + wandb.Status.MySQLStatus[key] = updatedStatus + err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore) + + return ctrlResult, err +} + +// external + +func externalMysqlInferStatus(ctx context.Context, c client.Client, wandb *apiv2.WeightsAndBiases, key string, newConditions []metav1.Condition, newInfraConn *apiv2.MysqlConnection) (ctrl.Result, error) { + statusBefore := wandb.DeepCopy().Status + oldStatus := wandb.Status.MySQLStatus[key] + oldInfraConn := oldStatus.Connection + state, ready, updatedConditions := external.InferExternalStatus(oldStatus.Conditions, newConditions, wandb.Generation, newInfraConn != nil) + conn := utils.Coalesce(newInfraConn, &oldInfraConn) + + wandb.Status.MySQLStatus[key] = apiv2.MysqlInfraStatus{ + WBInfraStatus: apiv2.WBInfraStatus{Ready: ready, State: state, Conditions: updatedConditions}, + Connection: *conn, + } + return ctrl.Result{}, updateWandbStatusIfChanged(ctx, c, wandb, statusBefore) +} + +// helpers + +func managedMysqlSpecNamespacedName(spec *apiv2.ManagedMysqlSpec) types.NamespacedName { + return types.NamespacedName{ + Namespace: spec.Namespace, + Name: spec.Name, + } +} + +// allMysqlInitSucceeded reports whether every managed MySQL instance has a +// successful database-initialization job. +func allMysqlInitSucceeded(wandb *apiv2.WeightsAndBiases) bool { + for key, spec := range wandb.Spec.MySQL { + if spec.ManagedMysql == nil { + continue + } + if !wandb.Status.Wandb.MySQLInit[key].Succeeded { + return false + } + } + return true +} + +// mysqlManifestConfig returns the manifest infra config for the instance key, +// falling back to the manifest "default" entry. +func mysqlManifestConfig(mfst manifest.Manifest, key string) manifest.InfraConfig { + cfg, _ := infraSizingConfig(mfst.Mysql, key) + return cfg +} + +func runMysqlInitJob(ctx context.Context, client client.Client, wandb *apiv2.WeightsAndBiases, mfst manifest.Manifest) (ctrl.Result, error) { + if wandb.Status.Wandb.MySQLInit == nil { + wandb.Status.Wandb.MySQLInit = map[string]apiv2.MigrationJobStatus{} + } + + var results []ctrl.Result + for key, spec := range wandb.Spec.MySQL { + if spec.ManagedMysql == nil { + continue + } + res, err := runMysqlInitJobInstance(ctx, client, wandb, key, spec.ManagedMysql, mfst) + if err != nil { + return ctrl.Result{}, err + } + results = append(results, res) + } + return consolidateResults(results), nil +} + +func runMysqlInitJobInstance(ctx context.Context, client client.Client, wandb *apiv2.WeightsAndBiases, key string, spec *apiv2.ManagedMysqlSpec, mfst manifest.Manifest) (ctrl.Result, error) { + if wandb.Status.Wandb.MySQLInit[key].Succeeded { + return ctrl.Result{}, nil + } + statusBefore := wandb.DeepCopy().Status + + logger := ctrl.LoggerFrom(ctx).WithName("mysqlInit").WithValues("instance", key) + + specNamespacedName := managedMysqlSpecNamespacedName(spec) + jobName := fmt.Sprintf("%s-moco-init", specNamespacedName.Name) + logger.Info("Checking for MySQL init job", "job", jobName) + job := &v1.Job{} + err := client.Get(ctx, types.NamespacedName{Name: jobName, Namespace: wandb.Namespace}, job) + + if err != nil && !errors.IsNotFound(err) { + return ctrl.Result{}, err + } + + if errors.IsNotFound(err) { + logger.Info("Creating MySQL init job") + + connSecretName := fmt.Sprintf("%s-connection", specNamespacedName.Name) + + // moco-writable has DDL/DML privileges on all non-system databases, + // so CREATE DATABASE works. The Oracle-era CREATE USER + GRANT steps + // are unnecessary — wandb connects directly as the secret's Username. + mysqlCmd := `mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -p"$MYSQL_PWD" ` + + `-e "CREATE DATABASE IF NOT EXISTS $MYSQL_DB;"` + + envFromConn := func(name, key string) corev1.EnvVar { + return corev1.EnvVar{ + Name: name, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: connSecretName}, + Key: key, + }, + }, + } + } + + job = &v1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: jobName, + Namespace: wandb.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + "app.kubernetes.io/component": "moco-init", + }, + }, + Spec: v1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyOnFailure, + Containers: []corev1.Container{ + { + Name: "moco-init", + Image: moco.MocoMySQLImage(mysqlManifestConfig(mfst, key).Images["mysql"], wandb.Spec.Global.ImageRegistry), + Command: []string{"/bin/sh", "-c", mysqlCmd}, + Env: []corev1.EnvVar{ + envFromConn("MYSQL_HOST", "Host"), + envFromConn("MYSQL_PORT", "Port"), + envFromConn("MYSQL_USER", "Username"), + envFromConn("MYSQL_PWD", "Password"), + envFromConn("MYSQL_DB", "Database"), + }, + }, + }, + }, + }, + }, + } + + if err := controllerutil.SetOwnerReference(wandb, job, client.Scheme()); err != nil { + return ctrl.Result{}, err + } + + if err := client.Create(ctx, job); err != nil { + return ctrl.Result{}, err + } + + wandb.Status.Wandb.MySQLInit[key] = apiv2.MigrationJobStatus{Name: jobName, Succeeded: false} + if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{RequeueAfter: defaultRequeueDuration}, nil + } + + if job.Status.Succeeded > 0 { + logger.Info("MySQL init job succeeded") + wandb.Status.Wandb.MySQLInit[key] = apiv2.MigrationJobStatus{Name: jobName, Succeeded: true} + if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + if job.Status.Failed > 0 { + logger.Info("MySQL init job failed") + wandb.Status.Wandb.MySQLInit[key] = apiv2.MigrationJobStatus{Name: jobName, Failed: true} + if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { + return ctrl.Result{}, err + } + // We might want to return an error or just requeue + return ctrl.Result{RequeueAfter: defaultRequeueDuration}, nil + } + + logger.Info("MySQL init job still running") + return ctrl.Result{RequeueAfter: defaultRequeueDuration}, nil +} diff --git a/internal/controller/reconciler/networking_cleanup.go b/internal/controller/reconciler/networking_cleanup.go new file mode 100644 index 00000000..efde2915 --- /dev/null +++ b/internal/controller/reconciler/networking_cleanup.go @@ -0,0 +1,57 @@ +package reconciler + +import ( + "context" + + apiv2 "github.com/wandb/operator/api/v2" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" +) + +func cleanupNetworkingModeResources( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, +) error { + switch wandb.Spec.Networking.Mode { + case apiv2.NetworkingModeIngress: + if err := deleteGateway(ctx, c, wandb); err != nil { + return err + } + if err := deleteInfraHTTPRoutes(ctx, c, wandb); err != nil { + return err + } + case apiv2.NetworkingModeGatewayAPI: + if err := deleteConsolidatedIngress(ctx, c, wandb); err != nil { + return err + } + if wandb.Spec.Networking.GatewayAPI == nil || !wandb.Spec.Networking.GatewayAPI.Gateway.Managed { + if err := deleteGateway(ctx, c, wandb); err != nil { + return err + } + } + case apiv2.NetworkingModeNone: + if err := deleteConsolidatedIngress(ctx, c, wandb); err != nil { + return err + } + if err := deleteGateway(ctx, c, wandb); err != nil { + return err + } + if err := deleteInfraHTTPRoutes(ctx, c, wandb); err != nil { + return err + } + } + + return nil +} + +func resetInactiveNetworkingStatus(wandb *apiv2.WeightsAndBiases) { + switch wandb.Spec.Networking.Mode { + case apiv2.NetworkingModeIngress: + wandb.Status.GatewayStatus = nil + case apiv2.NetworkingModeGatewayAPI: + wandb.Status.IngressStatus = nil + case apiv2.NetworkingModeNone: + wandb.Status.GatewayStatus = nil + wandb.Status.IngressStatus = nil + } +} diff --git a/internal/controller/reconciler/networking_route_builders_test.go b/internal/controller/reconciler/networking_route_builders_test.go new file mode 100644 index 00000000..e86ebf43 --- /dev/null +++ b/internal/controller/reconciler/networking_route_builders_test.go @@ -0,0 +1,146 @@ +package reconciler + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +var _ = Describe("Networking Route Builders", func() { + It("keeps managed gateway listeners namespace-scoped when infra stays local", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "wandb-ns"}, + Spec: apiv2.WeightsAndBiasesSpec{ + ObjectStore: map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: { + ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{Namespace: "wandb-ns"}, + }, + }, + }, + } + + allowedRoutes := buildAllowedRoutes(wandb) + Expect(allowedRoutes).NotTo(BeNil()) + Expect(allowedRoutes.Namespaces).NotTo(BeNil()) + Expect(allowedRoutes.Namespaces.From).NotTo(BeNil()) + Expect(*allowedRoutes.Namespaces.From).To(Equal(gatewayv1.NamespacesFromSame)) + }) + + It("widens managed gateway listeners when infra routes are cross-namespace", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "wandb-ns"}, + Spec: apiv2.WeightsAndBiasesSpec{ + ObjectStore: map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: { + ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{Namespace: "infra-ns"}, + }, + }, + }, + } + + allowedRoutes := buildAllowedRoutes(wandb) + Expect(allowedRoutes).NotTo(BeNil()) + Expect(allowedRoutes.Namespaces).NotTo(BeNil()) + Expect(allowedRoutes.Namespaces.From).NotTo(BeNil()) + Expect(*allowedRoutes.Namespaces.From).To(Equal(gatewayv1.NamespacesFromAll)) + }) + + It("builds infra HTTPRoutes with the resolved backend, labels, and parent refs", func() { + parentNamespace := gatewayv1.Namespace("gateway-ns") + parentRef := gatewayv1.ParentReference{ + Name: gatewayv1.ObjectName("shared-gateway"), + Namespace: &parentNamespace, + } + hostnames := []gatewayv1.Hostname{"wandb.example.com", "alt.example.com"} + entry := infraRouteEntry{ + name: "wandb-bucket-default", + namespace: "infra-ns", + serviceName: "wandb-seaweedfs-filer", + servicePort: gatewayv1.PortNumber(8333), + ingress: &serverManifest.AppIngressSpec{ + Paths: []string{"/bucket"}, + PathType: "Exact", + }, + } + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "wandb-ns"}, + } + + route := buildInfraHTTPRoute(wandb, parentRef, hostnames, entry) + + Expect(route.Namespace).To(Equal("infra-ns")) + Expect(route.Labels).To(HaveKeyWithValue(common.WandbNameLabel, "wandb")) + Expect(route.Labels).To(HaveKeyWithValue(common.WandbNamespaceLabel, "wandb-ns")) + Expect(route.Labels).To(HaveKeyWithValue(common.WandbComponentLabel, infraHTTPRouteComponent)) + Expect(route.Spec.ParentRefs).To(HaveLen(1)) + Expect(route.Spec.ParentRefs[0].Name).To(Equal(gatewayv1.ObjectName("shared-gateway"))) + Expect(route.Spec.ParentRefs[0].Namespace).NotTo(BeNil()) + Expect(*route.Spec.ParentRefs[0].Namespace).To(Equal(parentNamespace)) + Expect(route.Spec.Hostnames).To(Equal(hostnames)) + Expect(route.Spec.Rules).To(HaveLen(1)) + Expect(route.Spec.Rules[0].Matches).To(HaveLen(1)) + Expect(route.Spec.Rules[0].Matches[0].Path).NotTo(BeNil()) + Expect(*route.Spec.Rules[0].Matches[0].Path.Type).To(Equal(gatewayv1.PathMatchExact)) + Expect(*route.Spec.Rules[0].Matches[0].Path.Value).To(Equal("/bucket")) + Expect(route.Spec.Rules[0].BackendRefs).To(HaveLen(1)) + Expect(route.Spec.Rules[0].BackendRefs[0].Name).To(Equal(gatewayv1.ObjectName("wandb-seaweedfs-filer"))) + Expect(route.Spec.Rules[0].BackendRefs[0].Port).NotTo(BeNil()) + Expect(*route.Spec.Rules[0].BackendRefs[0].Port).To(Equal(gatewayv1.PortNumber(8333))) + }) + + It("builds application HTTPRoute templates for external gateways", func() { + listenerName := "https" + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "wandb-ns"}, + Spec: apiv2.WeightsAndBiasesSpec{ + Wandb: apiv2.WandbAppSpec{ + Hostname: "https://wandb.example.com", + AdditionalHostnames: []string{"alt.example.com"}, + }, + Networking: apiv2.NetworkingSpec{ + Mode: apiv2.NetworkingModeGatewayAPI, + GatewayAPI: &apiv2.GatewayAPIConfig{ + ListenerName: &listenerName, + }, + }, + }, + Status: apiv2.WeightsAndBiasesStatus{ + GatewayStatus: &apiv2.GatewayStatusSummary{ + GatewayRef: &apiv2.GatewayReference{ + Name: "shared-gateway", + Namespace: "gateway-ns", + }, + }, + }, + } + app := serverManifest.Application{ + Ingress: &serverManifest.AppIngressSpec{ + Paths: []string{"/api"}, + PathType: "Prefix", + ServicePort: "8080", + }, + } + + template := buildHTTPRouteTemplate(wandb, app) + + Expect(template).NotTo(BeNil()) + Expect(template.ParentRefs).To(HaveLen(1)) + Expect(template.ParentRefs[0].Name).To(Equal(gatewayv1.ObjectName("shared-gateway"))) + Expect(template.ParentRefs[0].Namespace).NotTo(BeNil()) + Expect(*template.ParentRefs[0].Namespace).To(Equal(gatewayv1.Namespace("gateway-ns"))) + Expect(template.ParentRefs[0].SectionName).NotTo(BeNil()) + Expect(*template.ParentRefs[0].SectionName).To(Equal(gatewayv1.SectionName("https"))) + Expect(template.Hostnames).To(ConsistOf( + gatewayv1.Hostname("wandb.example.com"), + gatewayv1.Hostname("alt.example.com"), + )) + Expect(template.Paths).To(Equal([]string{"/api"})) + Expect(template.PathType).To(Equal("Prefix")) + Expect(template.ServicePort).NotTo(BeNil()) + Expect(*template.ServicePort).To(Equal(gatewayv1.PortNumber(8080))) + }) +}) diff --git a/internal/controller/reconciler/objectstore.go b/internal/controller/reconciler/objectstore.go new file mode 100644 index 00000000..84d0bae3 --- /dev/null +++ b/internal/controller/reconciler/objectstore.go @@ -0,0 +1,285 @@ +package reconciler + +import ( + "context" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/external" + externalobjectstore "github.com/wandb/operator/internal/controller/infra/external/objectstore" + "github.com/wandb/operator/internal/controller/infra/managed/objectstore/seaweedfs" + "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/pkg/wandb/manifest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func objectStoreWriteState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + mfst manifest.Manifest, +) (map[string][]metav1.Condition, map[string]*apiv2.ObjectStoreConnection) { + outConds := map[string][]metav1.Condition{} + outConns := map[string]*apiv2.ObjectStoreConnection{} + for key, spec := range wandb.Spec.ObjectStore { + switch { + case spec.ManagedObjectStore != nil: + outConds[key], outConns[key] = managedObjectStoreWriteState(ctx, client, wandb, key, spec.ManagedObjectStore, mfst) + case spec.ExternalObjectStore != nil: + outConds[key], outConns[key] = externalobjectstore.WriteState(ctx, client, wandb, key, spec.ExternalObjectStore) + } + } + return outConds, outConns +} + +func objectStoreReadState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + conditions map[string][]metav1.Condition, +) map[string][]metav1.Condition { + out := map[string][]metav1.Condition{} + for key, spec := range wandb.Spec.ObjectStore { + switch { + case spec.ManagedObjectStore != nil: + out[key] = managedObjectStoreReadState(ctx, client, wandb, spec.ManagedObjectStore, conditions[key]) + case spec.ExternalObjectStore != nil: + out[key] = externalobjectstore.ReadState(ctx, client, wandb, key, conditions[key]) + default: + out[key] = conditions[key] + } + } + return out +} + +func objectStoreInferStatus( + ctx context.Context, + client client.Client, + recorder record.EventRecorder, + wandb *apiv2.WeightsAndBiases, + conditions map[string][]metav1.Condition, + infraConns map[string]*apiv2.ObjectStoreConnection, +) (ctrl.Result, error) { + if wandb.Status.ObjectStoreStatus == nil { + wandb.Status.ObjectStoreStatus = map[string]apiv2.ObjectStoreInfraStatus{} + } + var results []ctrl.Result + var firstErr error + for key, spec := range wandb.Spec.ObjectStore { + var res ctrl.Result + var err error + switch { + case spec.ManagedObjectStore != nil: + res, err = managedObjectStoreInferStatus(ctx, client, recorder, wandb, key, conditions[key], infraConns[key]) + case spec.ExternalObjectStore != nil: + res, err = externalObjectStoreInferStatus(ctx, client, wandb, key, conditions[key], infraConns[key]) + } + results = append(results, res) + if err != nil && firstErr == nil { + firstErr = err + } + } + return consolidateResults(results), firstErr +} + +func runObjectStoreRetentionFinalizer(ctx context.Context, c client.Client, wandb *apiv2.WeightsAndBiases, key string, spec apiv2.ObjectStoreSpec) error { + switch wandb.GetRetentionPolicy(objectStoreInstanceInfraSpec(spec)).OnDelete { + case apiv2.PurgeOnDelete: + return objectStorePurgeFinalizer(ctx, c, wandb, key, spec) + case apiv2.DetachOnDelete: + return objectStoreDetachFinalizer(ctx, c, wandb, key, spec) + } + return nil +} + +func objectStoreInstanceInfraSpec(spec apiv2.ObjectStoreSpec) apiv2.ManagedInfraSpec { + if spec.ManagedObjectStore != nil { + return spec.ManagedObjectStore.ManagedInfraSpec + } + return apiv2.ManagedInfraSpec{} +} + +func objectStorePurgeFinalizer( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + key string, + spec apiv2.ObjectStoreSpec, +) error { + if managed := spec.ManagedObjectStore; managed != nil { + onDeleteRule := seaweedfs.ToObjectStoreOnDeleteRule(wandb, wandb.GetRetentionPolicy(managed.ManagedInfraSpec)) + // Legacy MinIO predates multi-instance and is scoped to the CR, so only + // the default instance triggers its cleanup. + if key == apiv2.DefaultInstanceName { + _ = seaweedfs.CleanupLegacyMinio( + ctx, client, wandb.Name, wandb.Namespace, wandb.GetUID(), + true, onDeleteRule.Selector, + ) + } + specNamespacedName := managedObjectStoreSpecNamespacedName(managed) + return seaweedfs.PurgeFinalizer(ctx, client, specNamespacedName, onDeleteRule) + } + if spec.ExternalObjectStore != nil { + return externalobjectstore.DeleteConnectionSecret(ctx, client, wandb, key) + } + return nil +} + +func objectStoreDetachFinalizer( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + key string, + spec apiv2.ObjectStoreSpec, +) error { + managed := spec.ManagedObjectStore + if managed == nil { + return nil + } + if key == apiv2.DefaultInstanceName { + _ = seaweedfs.CleanupLegacyMinio( + ctx, client, wandb.Name, wandb.Namespace, wandb.GetUID(), + false, nil, + ) + } + specNamespacedName := managedObjectStoreSpecNamespacedName(managed) + return seaweedfs.DetachFinalizer(ctx, client, specNamespacedName, wandb) +} + +// managed + +func managedObjectStoreWriteState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + key string, + spec *apiv2.ManagedObjectStoreSpec, + mfst manifest.Manifest, +) ([]metav1.Condition, *apiv2.ObjectStoreConnection) { + log := ctrl.LoggerFrom(ctx) + var specNamespacedName = managedObjectStoreSpecNamespacedName(spec) + + retentionPolicy := wandb.GetRetentionPolicy(spec.ManagedInfraSpec) + onDeleteRule := seaweedfs.ToObjectStoreOnDeleteRule(wandb, retentionPolicy) + if key == apiv2.DefaultInstanceName { + if err := seaweedfs.CleanupLegacyMinio( + ctx, client, + wandb.Name, wandb.Namespace, wandb.GetUID(), + onDeleteRule.Policy == common.Purge, + onDeleteRule.Selector, + ); err != nil { + log.Error(err, "failed to clean up legacy MinIO resources") + } + } + + if conditions := seaweedfs.CheckDetached(ctx, client, specNamespacedName, wandb.GetUID(), spec.Replicas); conditions != nil { + return conditions, nil + } + + desiredCr, err := seaweedfs.ToObjectStoreVendorSpec(ctx, wandb, spec, client.Scheme(), mfst) + if err != nil { + log.Error(err, "failed to translate object store spec to vendor spec") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ControllerErrorReason, + }, + }, nil + } + + desiredConfig, err := seaweedfs.ToObjectStoreEnvConfig(ctx, *spec) + if err != nil { + log.Error(err, "failed to translate object store envConfig to vendor spec") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ControllerErrorReason, + }, + }, nil + } + + conditions, connection := seaweedfs.WriteState(ctx, client, specNamespacedName, desiredCr, desiredConfig, wandb) + return conditions, connection +} + +func managedObjectStoreReadState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedObjectStoreSpec, + newConditions []metav1.Condition, +) []metav1.Condition { + specNamespacedName := managedObjectStoreSpecNamespacedName(spec) + retentionPolicy := wandb.GetRetentionPolicy(spec.ManagedInfraSpec) + readConditions := seaweedfs.ReadState( + ctx, + client, + specNamespacedName, + seaweedfs.ToObjectStoreOnDeleteRule(wandb, retentionPolicy), + ) + newConditions = append(newConditions, readConditions...) + return newConditions +} + +func managedObjectStoreInferStatus( + ctx context.Context, + client client.Client, + recorder record.EventRecorder, + wandb *apiv2.WeightsAndBiases, + key string, + newConditions []metav1.Condition, + newInfraConn *apiv2.ObjectStoreConnection, +) (ctrl.Result, error) { + statusBefore := wandb.DeepCopy().Status + enabled := true + oldStatus := wandb.Status.ObjectStoreStatus[key] + oldConditions := oldStatus.Conditions + oldInfraConn := oldStatus.Connection + + updatedStatus, events, ctrlResult := seaweedfs.ComputeStatus( + ctx, + enabled, + oldConditions, + newConditions, + utils.Coalesce(newInfraConn, &oldInfraConn), + wandb.Generation, + ) + for _, e := range events { + recorder.Event(wandb, e.Type, e.Reason, e.Message) + } + wandb.Status.ObjectStoreStatus[key] = updatedStatus + err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore) + + return ctrlResult, err +} + +// external + +func externalObjectStoreInferStatus(ctx context.Context, c client.Client, wandb *apiv2.WeightsAndBiases, key string, newConditions []metav1.Condition, newInfraConn *apiv2.ObjectStoreConnection) (ctrl.Result, error) { + statusBefore := wandb.DeepCopy().Status + oldStatus := wandb.Status.ObjectStoreStatus[key] + oldInfraConn := oldStatus.Connection + state, ready, updatedConditions := external.InferExternalStatus(oldStatus.Conditions, newConditions, wandb.Generation, newInfraConn != nil) + conn := utils.Coalesce(newInfraConn, &oldInfraConn) + + wandb.Status.ObjectStoreStatus[key] = apiv2.ObjectStoreInfraStatus{ + WBInfraStatus: apiv2.WBInfraStatus{Ready: ready, State: state, Conditions: updatedConditions}, + Connection: *conn, + } + return ctrl.Result{}, updateWandbStatusIfChanged(ctx, c, wandb, statusBefore) +} + +// helpers + +func managedObjectStoreSpecNamespacedName(spec *apiv2.ManagedObjectStoreSpec) types.NamespacedName { + return types.NamespacedName{ + Namespace: spec.Namespace, + Name: spec.Name, + } +} diff --git a/internal/controller/reconciler/oidc_env_test.go b/internal/controller/reconciler/oidc_env_test.go new file mode 100644 index 00000000..e73f12d5 --- /dev/null +++ b/internal/controller/reconciler/oidc_env_test.go @@ -0,0 +1,126 @@ +package reconciler + +import ( + "context" + "testing" + + apiv2 "github.com/wandb/operator/api/v2" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func oidcTestCR() *apiv2.WeightsAndBiases { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default"}, + } + wandb.Spec.Wandb.OIDC = apiv2.OidcSpec{ + ClientId: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "my-oidc"}, + Key: "clientId", + }, + SessionLength: "48h", + } + return wandb +} + +func TestResolveCRFieldSecretSelector(t *testing.T) { + wandb := oidcTestCR() + + t.Run("returns selector for a secret-backed field", func(t *testing.T) { + sel, ok := resolveCRFieldSecretSelector(wandb, "spec.wandb.oidc.clientId") + if !ok { + t.Fatalf("expected clientId to resolve as a secret selector") + } + if sel.Name != "my-oidc" || sel.Key != "clientId" { + t.Fatalf("unexpected selector: %+v", sel) + } + }) + + t.Run("not found when selector is unset", func(t *testing.T) { + if _, ok := resolveCRFieldSecretSelector(wandb, "spec.wandb.oidc.clientSecret"); ok { + t.Fatalf("expected unset clientSecret to be treated as not found") + } + }) + + t.Run("not found for a plain string field", func(t *testing.T) { + if _, ok := resolveCRFieldSecretSelector(wandb, "spec.wandb.oidc.sessionLength"); ok { + t.Fatalf("expected string field not to resolve as a secret selector") + } + }) + + t.Run("not found for a missing path", func(t *testing.T) { + if _, ok := resolveCRFieldSecretSelector(wandb, "spec.wandb.nope"); ok { + t.Fatalf("expected missing path to be not found") + } + }) +} + +func TestResolveEnvvarsCustomResourceOIDC(t *testing.T) { + client := fake.NewClientBuilder().Build() + wandb := oidcTestCR() + + envs := []serverManifest.EnvVar{ + { + Name: "GORILLA_OIDC_CLIENT_ID", + Sources: []serverManifest.EnvSource{ + {Type: "custom-resource", Name: "oidc", Field: "spec.wandb.oidc.clientId"}, + }, + }, + { + Name: "GORILLA_SESSION_LENGTH", + Sources: []serverManifest.EnvSource{ + {Type: "custom-resource", Name: "oidc", Field: "spec.wandb.oidc.sessionLength"}, + }, + DefaultValue: "720h", + }, + } + + resolved, err := resolveEnvvars(context.Background(), client, wandb, serverManifest.Manifest{}, nil, envs) + if err != nil { + t.Fatalf("resolveEnvvars returned error: %v", err) + } + + clientID := mustFindEnvVar(t, resolved, "GORILLA_OIDC_CLIENT_ID") + if clientID.ValueFrom == nil || clientID.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected GORILLA_OIDC_CLIENT_ID to resolve from a secret key ref, got %+v", clientID) + } + if clientID.ValueFrom.SecretKeyRef.Name != "my-oidc" || clientID.ValueFrom.SecretKeyRef.Key != "clientId" { + t.Fatalf("unexpected client id secret ref: %+v", clientID.ValueFrom.SecretKeyRef) + } + + sessionLen := mustFindEnvVar(t, resolved, "GORILLA_SESSION_LENGTH") + if sessionLen.ValueFrom != nil { + t.Fatalf("expected GORILLA_SESSION_LENGTH to be a plain value, got valueFrom %+v", sessionLen.ValueFrom) + } + if sessionLen.Value != "48h" { + t.Fatalf("unexpected session length value: %q", sessionLen.Value) + } +} + +func TestResolveEnvvarsSessionLengthDefault(t *testing.T) { + client := fake.NewClientBuilder().Build() + // CR with no OIDC/session config set. + wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default"}} + + envs := []serverManifest.EnvVar{ + { + Name: "GORILLA_SESSION_LENGTH", + Sources: []serverManifest.EnvSource{ + {Type: "custom-resource", Name: "oidc", Field: "spec.wandb.oidc.sessionLength"}, + }, + DefaultValue: "720h", + }, + } + + resolved, err := resolveEnvvars(context.Background(), client, wandb, serverManifest.Manifest{}, nil, envs) + if err != nil { + t.Fatalf("resolveEnvvars returned error: %v", err) + } + + sessionLen := mustFindEnvVar(t, resolved, "GORILLA_SESSION_LENGTH") + if sessionLen.Value != "720h" { + t.Fatalf("expected session length to fall back to default, got %q", sessionLen.Value) + } +} \ No newline at end of file diff --git a/internal/controller/reconciler/pods.go b/internal/controller/reconciler/pods.go new file mode 100644 index 00000000..0bc6515f --- /dev/null +++ b/internal/controller/reconciler/pods.go @@ -0,0 +1,520 @@ +package reconciler + +import ( + "context" + "fmt" + "strings" + + v2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/logx" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + "k8s.io/api/core/v1" + "k8s.io/utils/ptr" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" +) + +const appWorkloadCapabilityAll v1.Capability = "ALL" + +func resolvePodSecurityContext() *v1.PodSecurityContext { + return &v1.PodSecurityContext{ + SeccompProfile: resolveRuntimeDefaultSeccompProfile(), + } +} + +func resolveContainerSecurityContext() *v1.SecurityContext { + return &v1.SecurityContext{ + AllowPrivilegeEscalation: ptr.To(false), + Capabilities: &v1.Capabilities{ + Drop: []v1.Capability{appWorkloadCapabilityAll}, + }, + SeccompProfile: resolveRuntimeDefaultSeccompProfile(), + } +} + +func resolveRuntimeDefaultSeccompProfile() *v1.SeccompProfile { + return &v1.SeccompProfile{ + Type: v1.SeccompProfileTypeRuntimeDefault, + } +} + +func resolveInitContainers(app serverManifest.Application, wandb *v2.WeightsAndBiases, envVars []v1.EnvVar, volumeMounts []v1.VolumeMount) []v1.Container { + initContainers := []v1.Container{} + + if app.InitContainers != nil { + for _, initContainerSpec := range app.InitContainers { + if initContainerSpec.Name == "migrate" { + continue + } + initContainer := v1.Container{ + Name: initContainerSpec.Name, + Image: initContainerSpec.Image.GetImage(wandb.Spec.Global.ImageRegistry), + Env: envVars, + Args: initContainerSpec.Args, + Command: initContainerSpec.Command, + VolumeMounts: volumeMounts, + SecurityContext: resolveContainerSecurityContext(), + } + initContainers = append(initContainers, initContainer) + } + } + return initContainers +} + +func resolveContainers(app serverManifest.Application, wandb *v2.WeightsAndBiases, envVars []v1.EnvVar, volumeMounts []v1.VolumeMount) []v1.Container { + // Build containers: support multi-container apps via app.Containers; fall back to a single + // default container when no explicit containers are provided. + containers := []v1.Container{} + if len(app.Containers) > 0 { + for _, container := range app.Containers { + // Convert ports + var containerPorts []v1.ContainerPort + for _, p := range container.Ports { + containerPorts = append(containerPorts, v1.ContainerPort{ + Name: p.Name, + ContainerPort: p.ContainerPort, + Protocol: p.Protocol, + }) + } + + // Choose image/args/command with sensible fallbacks to app-level values + img := app.Image.GetImage(wandb.Spec.Global.ImageRegistry) + if container.Image.Repository != "" { + img = container.Image.GetImage(wandb.Spec.Global.ImageRegistry) + } + args := app.Args + if len(container.Args) > 0 { + args = container.Args + } + cmd := app.Command + if len(container.Command) > 0 { + cmd = container.Command + } + + c := v1.Container{ + Name: container.Name, + Image: img, + Env: envVars, + Args: args, + Command: cmd, + Ports: containerPorts, + VolumeMounts: volumeMounts, + SecurityContext: resolveContainerSecurityContext(), + } + + if resources := ResolveResources(app, wandb, container.Resources); resources != nil { + c.Resources = *resources + } + + if container.StartupProbe != nil { + c.StartupProbe = container.StartupProbe.DeepCopy() + } + if container.LivenessProbe != nil { + c.LivenessProbe = container.LivenessProbe.DeepCopy() + } + if container.ReadinessProbe != nil { + c.ReadinessProbe = container.ReadinessProbe.DeepCopy() + } + + containers = append(containers, c) + } + } else { + // Backward-compatible single-container behavior + c := v1.Container{ + Name: app.Name, + Image: app.Image.GetImage(wandb.Spec.Global.ImageRegistry), + Env: envVars, + Args: app.Args, + Command: app.Command, + VolumeMounts: volumeMounts, + SecurityContext: resolveContainerSecurityContext(), + } + + if resources := ResolveResources(app, wandb, nil); resources != nil { + c.Resources = *resources + } + containers = append(containers, c) + } + return containers +} + +func resolveEnvvars(ctx context.Context, client ctrlClient.Client, wandb *v2.WeightsAndBiases, manifest serverManifest.Manifest, commonEnvs []string, envs []serverManifest.EnvVar) ([]v1.EnvVar, error) { + logger := logx.GetSlog(ctx) + var combinedEnvs []serverManifest.EnvVar + for _, commonVars := range commonEnvs { + if envvars, ok := manifest.CommonEnvvars[commonVars]; ok { + combinedEnvs = append(combinedEnvs, envvars...) + } + } + + for _, env := range envs { + for i, combinedEnv := range combinedEnvs { + if combinedEnv.Name == env.Name { + combinedEnvs = append(combinedEnvs[:i], combinedEnvs[i+1:]...) + break + } + } + } + + combinedEnvs = append(combinedEnvs, envs...) + + var envVars []v1.EnvVar + for _, env := range combinedEnvs { + // If a literal value is provided, it's a simple case. + if env.Value != "" { + envVars = append(envVars, v1.EnvVar{Name: env.Name, Value: env.Value}) + continue + } + if env.ValueFrom != nil { + envVars = append(envVars, v1.EnvVar{Name: env.Name, ValueFrom: env.ValueFrom}) + continue + } + + // Multi-source composition: build a comma-separated value from all resolvable sources. + // Secret-backed sources are exposed via intermediate env vars and referenced with $(VAR) expansion. + // If there is exactly one secret-backed source and no literals, keep direct SecretKeyRef for back-compat. + + // Temporary slices to build the final env value and intermediates + var components []string + var intermediateVars []v1.EnvVar + + // Helper to add a secret-backed component via an intermediate env var + addSecretComponent := func(selector v1.SecretKeySelector, idx int) { + // Deterministic name based on target env and source index + ivName := fmt.Sprintf("%s_%d", env.Name, idx) + // K8s env var names must be alphanumeric + _ and not start with a number + // The env.Name in manifest follows standard patterns; idx ensures uniqueness. + intermediateVars = append(intermediateVars, v1.EnvVar{ + Name: ivName, + ValueFrom: &v1.EnvVarSource{SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: selector.LocalObjectReference, + Key: selector.Key, + Optional: selector.Optional, + }}, + }) + components = append(components, fmt.Sprintf("$(%s)", ivName)) + } + + // Track if we only have a single secret-backed component + singleSecretSelector := v1.SecretKeySelector{} + secretOnlyCount := 0 + + for idx, src := range env.Sources { + switch src.Type { + case "generatedSecret": + if sel, ok := wandb.Status.GeneratedSecrets[src.Name]; ok { + singleSecretSelector = sel + secretOnlyCount++ + addSecretComponent(sel, idx) + } + case "mysql": + // MySQL connection URL as a secret ref. src.Name selects the + // instance, falling back to the default instance when empty or + // when the named instance has no status yet. + status, ok := v2.ResolveInstance(wandb.Status.MySQLStatus, src.Name) + if !ok { + continue + } + selector := status.Connection.URL + // Record for potential direct assignment case + singleSecretSelector = selector + secretOnlyCount++ + addSecretComponent(selector, idx) + case "redis": + status, ok := v2.ResolveInstance(wandb.Status.RedisStatus, src.Name) + if !ok { + continue + } + selector := status.Connection.URL + singleSecretSelector = selector + secretOnlyCount++ + addSecretComponent(selector, idx) + case "bucket": + status, ok := v2.ResolveInstance(wandb.Status.ObjectStoreStatus, src.Name) + if !ok { + continue + } + selector := v1.SecretKeySelector{ + LocalObjectReference: status.Connection.URL.LocalObjectReference, + } + switch src.Field { + case "host": + // Host/Port/Region are provider-dependent: absent for GCS/Azure + // and for plain AWS S3 (no custom endpoint / no region key). + // Marks optional so pods still start when the key is missing. + selector.Key = "Host" + selector.Optional = ptr.To(true) + case "port": + selector.Key = "Port" + selector.Optional = ptr.To(true) + case "region": + selector.Key = "Region" + selector.Optional = ptr.To(true) + default: + selector.Key = "url" + } + singleSecretSelector = selector + secretOnlyCount++ + addSecretComponent(selector, idx) + case "clickhouse": + // clickhouse fields are provided as separate keys in the same secret + status, ok := v2.ResolveInstance(wandb.Status.ClickHouseStatus, src.Name) + if !ok { + continue + } + selector := v1.SecretKeySelector{ + LocalObjectReference: status.Connection.URL.LocalObjectReference, + } + switch src.Field { + case "host": + selector.Key = "Host" + case "http-port": + selector.Key = "HTTPPort" + case "tcp-port": + selector.Key = "TCPPort" + case "user": + selector.Key = "User" + case "password": + selector.Key = "Password" + case "database": + selector.Key = "Database" + case "url": + selector.Key = "url" + default: + // Unrecognized field; skip + continue + } + singleSecretSelector = selector + secretOnlyCount++ + addSecretComponent(selector, idx) + case "kafka": + // kafka can be referenced as a full URL (no field) or by specific fields (host/port) + if src.Field == "" { + selector := wandb.Status.KafkaStatus.Connection.URL + singleSecretSelector = selector + secretOnlyCount++ + addSecretComponent(selector, idx) + break + } + selector := v1.SecretKeySelector{ + LocalObjectReference: wandb.Status.KafkaStatus.Connection.URL.LocalObjectReference, + } + switch src.Field { + case "host": + selector.Key = "Host" + case "port": + selector.Key = "Port" + default: + selector.Key = "url" + } + singleSecretSelector = selector + secretOnlyCount++ + addSecretComponent(selector, idx) + case "telemetry": + secretName := strings.TrimSpace(src.Name) + if secretName == "" { + secretName = strings.TrimSpace(wandb.Status.TelemetryStatus.Connection.ConnectionSecret) + } + if secretName == "" { + continue + } + + selector := v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: secretName, + }, + } + switch src.Field { + case "", "metrics", "metricsEndpoint": + selector.Key = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" + case "logs", "logsEndpoint": + selector.Key = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT" + case "traces", "tracesEndpoint": + selector.Key = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT" + case "metricsExporter": + selector.Key = "OTEL_METRICS_EXPORTER" + case "logsExporter": + selector.Key = "OTEL_LOGS_EXPORTER" + case "tracesExporter": + selector.Key = "OTEL_TRACES_EXPORTER" + case "protocol": + selector.Key = "OTEL_EXPORTER_OTLP_PROTOCOL" + case "serviceName": + selector.Key = "OTEL_SERVICE_NAME" + case "resourceAttributes": + selector.Key = "OTEL_RESOURCE_ATTRIBUTES" + case "gorillaTracer", "tracer": + selector.Key = "GORILLA_TRACER" + case "statsdAddress": + selector.Key = "GORILLA_STATSD_ADDRESS" + case "datadogTraceAgentURL", "ddTraceAgentURL": + selector.Key = "DD_TRACE_AGENT_URL" + case "datadogTraceAgentHost", "ddAgentHost": + selector.Key = "DD_AGENT_HOST" + case "datadogTraceAgentPort", "ddTraceAgentPort": + selector.Key = "DD_TRACE_AGENT_PORT" + default: + if strings.HasPrefix(src.Field, "OTEL_") { + selector.Key = src.Field + } else { + continue + } + } + + singleSecretSelector = selector + secretOnlyCount++ + addSecretComponent(selector, idx) + case "service": + // TODO(dpanzella): Determine if this is the right approach, I think writing the service to the + // applications status and reading from there is probably more correct + // Prefer deterministic manifest-derived service resolution to avoid startup races + // where the Service object has not been created yet. + if resolved, ok := manifest.ResolveServiceURL(src); ok { + components = append(components, resolved) + continue + } + + // Fallback: resolve from live Service object (back-compat). + serviceList := &v1.ServiceList{} + err := client.List( + ctx, + serviceList, + ctrlClient.InNamespace(wandb.Namespace), + ctrlClient.MatchingLabels{"app.kubernetes.io/name": src.Name}, + ) + if err != nil { + return nil, err + } + if len(serviceList.Items) == 0 || len(serviceList.Items[0].Spec.Ports) == 0 { + // Can't resolve; skip this component + continue + } + proto := "" + if src.Proto != "" { + proto = fmt.Sprintf("%s://", src.Proto) + } + // Choose a port: prefer named match if provided; else pick the first port + selectedPort := serviceList.Items[0].Spec.Ports[0].Port + if src.Port != "" { + for _, servicePort := range serviceList.Items[0].Spec.Ports { + if servicePort.Name == src.Port { + selectedPort = servicePort.Port + break + } + } + } + components = append(components, fmt.Sprintf("%s%s:%d%s", proto, serviceList.Items[0].Name, selectedPort, src.Path)) + case "jwt-issuer-map": + if wandb.Spec.Wandb.InternalServiceAuth.Enabled != nil && + *wandb.Spec.Wandb.InternalServiceAuth.Enabled { + // TODO Get real OIDC Issuer + issuer := "https://kubernetes.default.svc.cluster.local" + if wandb.Spec.Wandb.InternalServiceAuth.OIDCIssuer != "" { + issuer = wandb.Spec.Wandb.InternalServiceAuth.OIDCIssuer + } + components = append( + components, + fmt.Sprintf( + "{\"system:serviceaccount:%s:%s\": \"%s\" }", + wandb.Namespace, + wandb.Spec.Wandb.ServiceAccount.ServiceAccountName, + issuer, + ), + ) + } + case "custom-resource": + // Read a value from the current WandB custom resource via dotted field path + if src.Field == "" { + // No field specified; nothing to resolve + continue + } + // secret component + if sel, ok := resolveCRFieldSecretSelector(wandb, src.Field); ok { + singleSecretSelector = sel + secretOnlyCount++ + addSecretComponent(sel, idx) + } else if val, ok := resolveCRFieldString(wandb, src.Field); ok { + // Treat as a literal component (not secret-backed) + logger.Debug("field found in CR", "cr", wandb.Name, "field", src.Field, "value", val) + components = append(components, val) + } else { + logger.Debug("field not found in CR", "cr", wandb.Name, "field", src.Field) + } + default: + // Unknown source type; skip + continue + } + } + + // If we built no components, skip emitting this env var + if len(components) == 0 { + if env.DefaultValue != "" { + envVars = append(envVars, v1.EnvVar{Name: env.Name, Value: env.DefaultValue}) + } + continue + } + + // Optimization/back-compat: if there's exactly one component and it is secret-backed, emit ValueFrom directly + if len(components) == 1 && secretOnlyCount == 1 && components[0] != "" && intermediateVars != nil { + // Emit the single env var directly from the secret without intermediate + envVars = append(envVars, v1.EnvVar{ + Name: env.Name, + ValueFrom: &v1.EnvVarSource{SecretKeyRef: &singleSecretSelector}, + }) + continue + } + + // Otherwise, add all intermediate vars first to ensure $(VAR) expansion works + envVars = append(envVars, intermediateVars...) + // Then add the final composed env var + envVars = append(envVars, v1.EnvVar{ + Name: env.Name, + Value: strings.Join(components, ","), + }) + } + return envVars, nil +} + +func resolveVolumeMounts(ctx context.Context, manifest serverManifest.Manifest, commonvms []string, vms []serverManifest.VolumeMount) ([]v1.Volume, []v1.VolumeMount, error) { + log := logx.GetSlog(ctx) + + var combinedVolumeMounts []serverManifest.VolumeMount + for _, commonVolumeMounts := range commonvms { + if volumeMounts, ok := manifest.CommonVolumeMounts[commonVolumeMounts]; ok { + combinedVolumeMounts = append(combinedVolumeMounts, volumeMounts...) + } + } + + combinedVolumeMounts = append(combinedVolumeMounts, vms...) + + var volumes []v1.Volume + var volumeMounts []v1.VolumeMount + for _, manifestVM := range combinedVolumeMounts { + volume := v1.Volume{ + Name: manifestVM.Name, + } + switch manifestVM.Source.Type { + case "secret": + volume.Secret = &v1.SecretVolumeSource{ + SecretName: manifestVM.Source.Name, + } + case "configMap": + volume.ConfigMap = &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: manifestVM.Source.Name, + }, + } + case "emptyDir": + volume.EmptyDir = &v1.EmptyDirVolumeSource{} + default: + log.Error("unsupported volume source type", "type", manifestVM.Source.Type) + return nil, nil, fmt.Errorf("unsupported volume source type: %s", manifestVM.Source.Type) + } + volumeMount := v1.VolumeMount{ + MountPath: manifestVM.MountPath, + Name: manifestVM.Name, + } + volumes = append(volumes, volume) + volumeMounts = append(volumeMounts, volumeMount) + } + return volumes, volumeMounts, nil +} diff --git a/internal/controller/reconciler/pods_instance_test.go b/internal/controller/reconciler/pods_instance_test.go new file mode 100644 index 00000000..90ed00b0 --- /dev/null +++ b/internal/controller/reconciler/pods_instance_test.go @@ -0,0 +1,80 @@ +package reconciler + +import ( + "context" + "testing" + + apiv2 "github.com/wandb/operator/api/v2" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func mysqlURLSelector(secretName string) corev1.SecretKeySelector { + return corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + Key: "url", + } +} + +func wandbWithTwoMysqlInstances() *apiv2.WeightsAndBiases { + return &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wb", Namespace: "default"}, + Status: apiv2.WeightsAndBiasesStatus{ + MySQLStatus: map[string]apiv2.MysqlInfraStatus{ + apiv2.DefaultInstanceName: {Connection: apiv2.MysqlConnection{URL: mysqlURLSelector("default-conn")}}, + "analytics": {Connection: apiv2.MysqlConnection{URL: mysqlURLSelector("analytics-conn")}}, + }, + }, + } +} + +func resolveSingleMysqlEnv(t *testing.T, wandb *apiv2.WeightsAndBiases, instance string) corev1.EnvVar { + t.Helper() + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + client := fake.NewClientBuilder().WithScheme(scheme).Build() + + envs := []serverManifest.EnvVar{ + {Name: "MYSQL", Sources: []serverManifest.EnvSource{{Type: "mysql", Name: instance}}}, + } + resolved, err := resolveEnvvars(context.Background(), client, wandb, serverManifest.Manifest{}, nil, envs) + if err != nil { + t.Fatalf("resolveEnvvars returned error: %v", err) + } + return mustFindEnvVar(t, resolved, "MYSQL") +} + +func TestResolveEnvvarsMysqlRoutesToNamedInstance(t *testing.T) { + env := resolveSingleMysqlEnv(t, wandbWithTwoMysqlInstances(), "analytics") + if env.ValueFrom == nil || env.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected secret-backed env var, got %+v", env) + } + if got := env.ValueFrom.SecretKeyRef.Name; got != "analytics-conn" { + t.Fatalf("expected analytics-conn, got %q", got) + } +} + +func TestResolveEnvvarsMysqlEmptyInstanceUsesDefault(t *testing.T) { + env := resolveSingleMysqlEnv(t, wandbWithTwoMysqlInstances(), "") + if env.ValueFrom == nil || env.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected secret-backed env var, got %+v", env) + } + if got := env.ValueFrom.SecretKeyRef.Name; got != "default-conn" { + t.Fatalf("expected default-conn, got %q", got) + } +} + +func TestResolveEnvvarsMysqlMissingInstanceFallsBackToDefault(t *testing.T) { + env := resolveSingleMysqlEnv(t, wandbWithTwoMysqlInstances(), "does-not-exist") + if env.ValueFrom == nil || env.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected secret-backed env var, got %+v", env) + } + if got := env.ValueFrom.SecretKeyRef.Name; got != "default-conn" { + t.Fatalf("expected fallback to default-conn, got %q", got) + } +} diff --git a/internal/controller/reconciler/pods_test.go b/internal/controller/reconciler/pods_test.go new file mode 100644 index 00000000..879301d4 --- /dev/null +++ b/internal/controller/reconciler/pods_test.go @@ -0,0 +1,48 @@ +package reconciler + +import ( + "testing" + + apiv2 "github.com/wandb/operator/api/v2" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" +) + +func testWeightsAndBiases() *apiv2.WeightsAndBiases { + return &apiv2.WeightsAndBiases{} +} + +func TestResolveContainersPreservesResolvedProbes(t *testing.T) { + containers := resolveContainers(serverManifest.Application{ + Name: "worker", + Image: serverManifest.ImageRef{ + Repository: "worker", + Tag: "test", + }, + Containers: []serverManifest.ContainerSpec{ + { + Name: "worker", + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{Command: []string{"true"}}, + }, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{}, + }, + }, + }, + }, + }, testWeightsAndBiases(), nil, nil) + + if len(containers) != 1 { + t.Fatalf("expected one container, got %d", len(containers)) + } + if containers[0].LivenessProbe == nil || containers[0].LivenessProbe.Exec == nil { + t.Fatalf("expected exec liveness probe to be preserved, got %+v", containers[0].LivenessProbe) + } + if containers[0].ReadinessProbe == nil || containers[0].ReadinessProbe.TCPSocket == nil { + t.Fatalf("expected TCP readiness probe to be preserved, got %+v", containers[0].ReadinessProbe) + } +} diff --git a/internal/controller/reconciler/probe_defaults.go b/internal/controller/reconciler/probe_defaults.go new file mode 100644 index 00000000..9a1dac56 --- /dev/null +++ b/internal/controller/reconciler/probe_defaults.go @@ -0,0 +1,103 @@ +package reconciler + +import ( + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/probes" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func applyWandbProbeDefaults(app serverManifest.Application, defaults apiv2.WandbProbeDefaults) serverManifest.Application { + if len(app.Containers) == 0 { + return app + } + + result := app + result.Containers = make([]serverManifest.ContainerSpec, len(app.Containers)) + for i, container := range app.Containers { + result.Containers[i] = applyContainerProbeDefaults(container, defaults) + } + return result +} + +func applyContainerProbeDefaults( + container serverManifest.ContainerSpec, + defaults apiv2.WandbProbeDefaults, +) serverManifest.ContainerSpec { + result := container + result.StartupProbe = probes.Clone(container.StartupProbe) + result.LivenessProbe = applyProbeTemplate(container.LivenessProbe, defaults.LivenessProbe) + result.ReadinessProbe = applyProbeTemplate(container.ReadinessProbe, defaults.ReadinessProbe) + + startupHandlerSource := result.ReadinessProbe + if !probes.HasHandler(startupHandlerSource) { + startupHandlerSource = result.LivenessProbe + } + result.StartupProbe = applyStartupProbeTemplate( + result.StartupProbe, + startupHandlerSource, + defaults.StartupProbe, + ) + + normalizeContainerHTTPGetProbePorts(&result) + return result +} + +func applyProbeTemplate(target, template *corev1.Probe) *corev1.Probe { + result := probes.ApplyTemplate(target, template) + if !probes.HasHandler(result) { + return nil + } + return result +} + +func applyStartupProbeTemplate(target, handlerSource, template *corev1.Probe) *corev1.Probe { + if target == nil && template == nil { + return nil + } + result := probes.ApplyTemplate(target, template) + if !probes.HasHandler(result) && handlerSource != nil { + if result == nil { + result = &corev1.Probe{} + } + probes.MergeHandlerMissing(&result.ProbeHandler, handlerSource.ProbeHandler) + } + if !probes.HasHandler(result) { + return nil + } + return result +} + +func normalizeContainerHTTPGetProbePorts(container *serverManifest.ContainerSpec) { + if container == nil { + return + } + + normalizeHTTPGetProbePort(container.StartupProbe, container.Ports) + normalizeHTTPGetProbePort(container.LivenessProbe, container.Ports) + normalizeHTTPGetProbePort(container.ReadinessProbe, container.Ports) +} + +func normalizeHTTPGetProbePort(probe *corev1.Probe, ports []serverManifest.ContainerPort) { + if probe == nil || probe.HTTPGet == nil || !probes.IntOrStringEmpty(probe.HTTPGet.Port) { + return + } + if port, ok := defaultHTTPGetProbePort(ports); ok { + probe.HTTPGet.Port = port + } +} + +func defaultHTTPGetProbePort(ports []serverManifest.ContainerPort) (intstr.IntOrString, bool) { + for _, port := range ports { + if port.Name != "" { + return intstr.FromString(port.Name), true + } + } + for _, port := range ports { + if port.ContainerPort != 0 { + return intstr.FromInt(int(port.ContainerPort)), true + } + } + return intstr.IntOrString{}, false +} diff --git a/internal/controller/reconciler/probe_defaults_test.go b/internal/controller/reconciler/probe_defaults_test.go new file mode 100644 index 00000000..74796b74 --- /dev/null +++ b/internal/controller/reconciler/probe_defaults_test.go @@ -0,0 +1,371 @@ +package reconciler + +import ( + "testing" + + apiv2 "github.com/wandb/operator/api/v2" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func defaultedProbeDefaults() apiv2.WandbProbeDefaults { + return apiv2.WandbProbeDefaults{ + StartupProbe: &corev1.Probe{ + PeriodSeconds: 5, + TimeoutSeconds: 2, + FailureThreshold: 24, + SuccessThreshold: 1, + }, + LivenessProbe: &corev1.Probe{ + PeriodSeconds: 10, + TimeoutSeconds: 1, + FailureThreshold: 3, + SuccessThreshold: 1, + }, + ReadinessProbe: &corev1.Probe{ + PeriodSeconds: 10, + TimeoutSeconds: 1, + FailureThreshold: 3, + SuccessThreshold: 1, + }, + } +} + +func TestApplyWandbProbeDefaultsDerivesStartupFromReadiness(t *testing.T) { + app := serverManifest.Application{ + Name: "api", + Containers: []serverManifest.ContainerSpec{{ + Name: "api", + Ports: []serverManifest.ContainerPort{{ + Name: "api", + ContainerPort: 8080, + Protocol: corev1.ProtocolTCP, + }}, + LivenessProbe: &corev1.Probe{ + PeriodSeconds: 99, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/healthz"}, + }, + }, + ReadinessProbe: &corev1.Probe{ + PeriodSeconds: 77, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/ready", + Host: "127.0.0.1", + Scheme: corev1.URISchemeHTTPS, + HTTPHeaders: []corev1.HTTPHeader{ + {Name: "X-Probe", Value: "readiness"}, + }, + }, + }, + }, + }}, + } + + result := applyWandbProbeDefaults(app, defaultedProbeDefaults()) + container := result.Containers[0] + startupProbe := container.StartupProbe + if startupProbe == nil || startupProbe.HTTPGet == nil { + t.Fatalf("expected startup probe derived from readiness") + } + if startupProbe.HTTPGet.Path != "/ready" { + t.Fatalf("expected startup probe to use readiness path, got %s", startupProbe.HTTPGet.Path) + } + if startupProbe.HTTPGet.Host != "127.0.0.1" { + t.Fatalf("unexpected startup host: %s", startupProbe.HTTPGet.Host) + } + if startupProbe.HTTPGet.Scheme != corev1.URISchemeHTTPS { + t.Fatalf("unexpected startup scheme: %s", startupProbe.HTTPGet.Scheme) + } + if len(startupProbe.HTTPGet.HTTPHeaders) != 1 || startupProbe.HTTPGet.HTTPHeaders[0].Name != "X-Probe" { + t.Fatalf("expected startup probe to preserve readiness HTTP headers: %+v", startupProbe.HTTPGet.HTTPHeaders) + } + if startupProbe.HTTPGet.Port != intstr.FromString("api") { + t.Fatalf("expected startup probe port to default to named port, got %#v", startupProbe.HTTPGet.Port) + } + if startupProbe.InitialDelaySeconds != 0 { + t.Fatalf("expected startup probe to avoid initial delay, got %d", startupProbe.InitialDelaySeconds) + } + if startupProbe.PeriodSeconds != 5 || + startupProbe.TimeoutSeconds != 2 || + startupProbe.FailureThreshold != 24 { + t.Fatalf("unexpected startup timings: %+v", startupProbe) + } + if container.ReadinessProbe.PeriodSeconds != 77 { + t.Fatalf("expected readiness timing to remain explicit, got %+v", container.ReadinessProbe) + } + if app.Containers[0].StartupProbe != nil { + t.Fatalf("expected original application to remain unchanged") + } +} + +func TestApplyWandbProbeDefaultsUsesLivenessWhenReadinessIsMissing(t *testing.T) { + app := serverManifest.Application{ + Name: "api", + Containers: []serverManifest.ContainerSpec{{ + Name: "api", + Ports: []serverManifest.ContainerPort{{ContainerPort: 8080}}, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/healthz"}, + }, + }, + }}, + } + + result := applyWandbProbeDefaults(app, defaultedProbeDefaults()) + startupProbe := result.Containers[0].StartupProbe + if startupProbe == nil || startupProbe.HTTPGet == nil { + t.Fatalf("expected startup probe derived from liveness") + } + if startupProbe.HTTPGet.Path != "/healthz" { + t.Fatalf("expected startup probe to use liveness path, got %s", startupProbe.HTTPGet.Path) + } + if startupProbe.HTTPGet.Port != intstr.FromInt(8080) { + t.Fatalf("expected startup probe port to default to numeric port, got %#v", startupProbe.HTTPGet.Port) + } +} + +func TestApplyWandbProbeDefaultsUsesDefaultedCRDProbeTemplate(t *testing.T) { + app := serverManifest.Application{ + Name: "api", + Containers: []serverManifest.ContainerSpec{{ + Name: "api", + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/ready"}, + }, + }, + }}, + } + + result := applyWandbProbeDefaults(app, apiv2.WandbProbeDefaults{ + StartupProbe: &corev1.Probe{ + PeriodSeconds: 2, + TimeoutSeconds: 2, + FailureThreshold: 60, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/healthz/initialized", + Port: intstr.FromInt(8081), + }, + }, + }, + }) + + startupProbe := result.Containers[0].StartupProbe + if startupProbe == nil || startupProbe.HTTPGet == nil { + t.Fatalf("expected startup probe from CRD template") + } + if startupProbe.HTTPGet.Path != "/healthz/initialized" { + t.Fatalf("unexpected startup path: %s", startupProbe.HTTPGet.Path) + } + if startupProbe.HTTPGet.Port != intstr.FromInt(8081) { + t.Fatalf("unexpected startup port: %#v", startupProbe.HTTPGet.Port) + } + if startupProbe.PeriodSeconds != 2 || startupProbe.FailureThreshold != 60 { + t.Fatalf("expected CRD startup timings to win, got %+v", startupProbe) + } + if startupProbe.TimeoutSeconds != 2 { + t.Fatalf("expected defaulted timeout from CRD template, got %d", startupProbe.TimeoutSeconds) + } +} + +func TestApplyWandbProbeDefaultsPreservesExplicitStartupProbe(t *testing.T) { + app := serverManifest.Application{ + Name: "api", + Containers: []serverManifest.ContainerSpec{{ + Name: "api", + Ports: []serverManifest.ContainerPort{{Name: "api", ContainerPort: 8080}}, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/ready"}, + }, + }, + StartupProbe: &corev1.Probe{ + PeriodSeconds: 3, + FailureThreshold: 9, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/startup"}, + }, + }, + }}, + } + + result := applyWandbProbeDefaults(app, apiv2.WandbProbeDefaults{ + StartupProbe: &corev1.Probe{ + PeriodSeconds: 2, + TimeoutSeconds: 2, + FailureThreshold: 24, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/healthz/initialized"}, + }, + }, + }) + + startupProbe := result.Containers[0].StartupProbe + if startupProbe == nil || startupProbe.HTTPGet == nil { + t.Fatalf("expected explicit startup probe") + } + if startupProbe.HTTPGet.Path != "/startup" { + t.Fatalf("expected explicit startup path to win, got %s", startupProbe.HTTPGet.Path) + } + if startupProbe.HTTPGet.Port != intstr.FromString("api") { + t.Fatalf("expected startup probe port to default to named port, got %#v", startupProbe.HTTPGet.Port) + } + if startupProbe.PeriodSeconds != 3 || startupProbe.FailureThreshold != 9 { + t.Fatalf("expected explicit startup timings to win, got %+v", startupProbe) + } + if startupProbe.TimeoutSeconds != 2 { + t.Fatalf("expected missing timeout to use CRD template default, got %d", startupProbe.TimeoutSeconds) + } +} + +func TestApplyWandbProbeDefaultsMergesDefaultedTemplatesForAllProbeTypes(t *testing.T) { + app := serverManifest.Application{ + Name: "api", + Containers: []serverManifest.ContainerSpec{{ + Name: "api", + Ports: []serverManifest.ContainerPort{{ + Name: "api", + ContainerPort: 8080, + }}, + StartupProbe: &corev1.Probe{ + PeriodSeconds: 3, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/startup"}, + }, + }, + LivenessProbe: &corev1.Probe{ + TimeoutSeconds: 7, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/healthz"}, + }, + }, + ReadinessProbe: &corev1.Probe{ + FailureThreshold: 9, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/ready"}, + }, + }, + }}, + } + + result := applyWandbProbeDefaults(app, defaultedProbeDefaults()) + container := result.Containers[0] + + if container.StartupProbe.HTTPGet.Path != "/startup" || + container.LivenessProbe.HTTPGet.Path != "/healthz" || + container.ReadinessProbe.HTTPGet.Path != "/ready" { + t.Fatalf("expected explicit probe handlers to win, got startup=%+v liveness=%+v readiness=%+v", + container.StartupProbe, container.LivenessProbe, container.ReadinessProbe) + } + if container.StartupProbe.HTTPGet.Port != intstr.FromString("api") || + container.LivenessProbe.HTTPGet.Port != intstr.FromString("api") || + container.ReadinessProbe.HTTPGet.Port != intstr.FromString("api") { + t.Fatalf("expected HTTP ports to default for all probe types, got startup=%#v liveness=%#v readiness=%#v", + container.StartupProbe.HTTPGet.Port, + container.LivenessProbe.HTTPGet.Port, + container.ReadinessProbe.HTTPGet.Port) + } + if container.StartupProbe.PeriodSeconds != 3 || + container.StartupProbe.TimeoutSeconds != 2 || + container.StartupProbe.FailureThreshold != 24 || + container.StartupProbe.SuccessThreshold != 1 { + t.Fatalf("unexpected startup probe merge result: %+v", container.StartupProbe) + } + if container.LivenessProbe.PeriodSeconds != 10 || + container.LivenessProbe.TimeoutSeconds != 7 || + container.LivenessProbe.FailureThreshold != 3 || + container.LivenessProbe.SuccessThreshold != 1 { + t.Fatalf("unexpected liveness probe merge result: %+v", container.LivenessProbe) + } + if container.ReadinessProbe.PeriodSeconds != 10 || + container.ReadinessProbe.TimeoutSeconds != 1 || + container.ReadinessProbe.FailureThreshold != 9 || + container.ReadinessProbe.SuccessThreshold != 1 { + t.Fatalf("unexpected readiness probe merge result: %+v", container.ReadinessProbe) + } +} + +func TestApplyWandbProbeDefaultsMergesLivenessAndReadinessTemplates(t *testing.T) { + app := serverManifest.Application{ + Name: "api", + Containers: []serverManifest.ContainerSpec{{ + Name: "api", + Ports: []serverManifest.ContainerPort{{ + Name: "api", + ContainerPort: 8080, + }}, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/healthz"}, + }, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/ready"}, + }, + }, + }}, + } + + result := applyWandbProbeDefaults(app, apiv2.WandbProbeDefaults{ + LivenessProbe: &corev1.Probe{ + TimeoutSeconds: 4, + FailureThreshold: 5, + }, + ReadinessProbe: &corev1.Probe{ + PeriodSeconds: 3, + SuccessThreshold: 2, + }, + }) + + container := result.Containers[0] + if container.LivenessProbe.TimeoutSeconds != 4 || container.LivenessProbe.FailureThreshold != 5 { + t.Fatalf("expected liveness template fields to be applied, got %+v", container.LivenessProbe) + } + if container.ReadinessProbe.PeriodSeconds != 3 || container.ReadinessProbe.SuccessThreshold != 2 { + t.Fatalf("expected readiness template fields to be applied, got %+v", container.ReadinessProbe) + } + if container.LivenessProbe.HTTPGet.Port != intstr.FromString("api") { + t.Fatalf("expected liveness HTTP port default, got %#v", container.LivenessProbe.HTTPGet.Port) + } +} + +func TestApplyWandbProbeDefaultsDoesNotCreateInvalidStartupProbe(t *testing.T) { + app := serverManifest.Application{ + Name: "worker", + Containers: []serverManifest.ContainerSpec{{ + Name: "worker", + }}, + } + + result := applyWandbProbeDefaults(app, defaultedProbeDefaults()) + container := result.Containers[0] + if container.StartupProbe != nil || container.LivenessProbe != nil || container.ReadinessProbe != nil { + t.Fatalf("expected no invalid probes, got startup=%+v liveness=%+v readiness=%+v", + container.StartupProbe, container.LivenessProbe, container.ReadinessProbe) + } +} + +func TestApplyWandbProbeDefaultsDoesNotCreateStartupWithoutDefaultTemplate(t *testing.T) { + app := serverManifest.Application{ + Name: "api", + Containers: []serverManifest.ContainerSpec{{ + Name: "api", + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/healthz"}, + }, + }, + }}, + } + + result := applyWandbProbeDefaults(app, apiv2.WandbProbeDefaults{}) + if result.Containers[0].StartupProbe != nil { + t.Fatalf("expected no startup probe without a defaulted CR template, got %+v", result.Containers[0].StartupProbe) + } +} diff --git a/internal/controller/reconciler/rbac.go b/internal/controller/reconciler/rbac.go new file mode 100644 index 00000000..66dcd298 --- /dev/null +++ b/internal/controller/reconciler/rbac.go @@ -0,0 +1,254 @@ +package reconciler + +import ( + "context" + "fmt" + + "github.com/wandb/operator/api/v2" + "k8s.io/api/core/v1" + v4 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + v3 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +// createOrUpdateServiceAccount creates or updates the ServiceAccount for the W&B applications +func createOrUpdateServiceAccount( + ctx context.Context, + client client.Client, + wandb *v2.WeightsAndBiases, + serviceAccountName string, +) error { + log := controllerruntime.LoggerFrom(ctx) + + serviceAccount := &v1.ServiceAccount{ + ObjectMeta: v3.ObjectMeta{ + Name: serviceAccountName, + Namespace: wandb.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + "app.kubernetes.io/part-of": "wandb", + }, + Annotations: wandb.Spec.Wandb.ServiceAccount.Annotations, + }, + AutomountServiceAccountToken: ptr.To(true), + } + + if err := controllerutil.SetControllerReference(wandb, serviceAccount, client.Scheme()); err != nil { + return fmt.Errorf("failed to set owner reference on ServiceAccount: %w", err) + } + + existingServiceAccount := &v1.ServiceAccount{} + if err := client.Get(ctx, types.NamespacedName{Name: serviceAccountName, Namespace: wandb.Namespace}, existingServiceAccount); err != nil { + if errors.IsNotFound(err) { + log.Info("Creating a new ServiceAccount", "Namespace", wandb.Namespace, "Name", serviceAccountName) + if err := client.Create(ctx, serviceAccount); err != nil { + return fmt.Errorf("failed to create ServiceAccount: %w", err) + } + return nil + } + return fmt.Errorf("failed to get existing ServiceAccount: %w", err) + } + + existingServiceAccount.Annotations = serviceAccount.Annotations + existingServiceAccount.Labels = serviceAccount.Labels + existingServiceAccount.OwnerReferences = serviceAccount.OwnerReferences + existingServiceAccount.AutomountServiceAccountToken = serviceAccount.AutomountServiceAccountToken + log.Info("Updating existing ServiceAccount", "Namespace", wandb.Namespace, "Name", serviceAccountName) + if err := client.Update(ctx, existingServiceAccount); err != nil { + return fmt.Errorf("failed to update ServiceAccount: %w", err) + } + + return nil +} + +// createOrUpdateRole creates or updates the Role for the W&B service account +func createOrUpdateRole( + ctx context.Context, + client client.Client, + wandb *v2.WeightsAndBiases, + serviceAccountName string, +) error { + log := controllerruntime.LoggerFrom(ctx) + + role := &v4.Role{ + ObjectMeta: v3.ObjectMeta{ + Name: serviceAccountName, + Namespace: wandb.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + "app.kubernetes.io/part-of": "wandb", + }, + }, + Rules: []v4.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"get", "create", "update", "delete"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list"}, + }, + }, + } + + if err := controllerutil.SetOwnerReference(wandb, role, client.Scheme()); err != nil { + return fmt.Errorf("failed to set owner reference on Role: %w", err) + } + + existingRole := &v4.Role{} + err := client.Get(ctx, types.NamespacedName{Name: serviceAccountName, Namespace: wandb.Namespace}, existingRole) + if err != nil { + if errors.IsNotFound(err) { + log.Info("Creating Role", "name", serviceAccountName, "namespace", wandb.Namespace) + if err := client.Create(ctx, role); err != nil { + return fmt.Errorf("failed to create Role: %w", err) + } + return nil + } + return fmt.Errorf("failed to get Role: %w", err) + } + + // Update existing role + existingRole.Rules = role.Rules + existingRole.Labels = role.Labels + log.Info("Updating Role", "name", serviceAccountName, "namespace", wandb.Namespace) + if err := client.Update(ctx, existingRole); err != nil { + return fmt.Errorf("failed to update Role: %w", err) + } + + return nil +} + +// createOrUpdateRoleBinding creates or updates the RoleBinding for the W&B service account +func createOrUpdateRoleBinding( + ctx context.Context, + client client.Client, + wandb *v2.WeightsAndBiases, + serviceAccountName string, +) error { + log := controllerruntime.LoggerFrom(ctx) + + roleBinding := &v4.RoleBinding{ + ObjectMeta: v3.ObjectMeta{ + Name: serviceAccountName, + Namespace: wandb.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + "app.kubernetes.io/part-of": "wandb", + }, + }, + RoleRef: v4.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: serviceAccountName, + }, + Subjects: []v4.Subject{ + { + Kind: "ServiceAccount", + Name: serviceAccountName, + Namespace: wandb.Namespace, + }, + }, + } + + if err := controllerutil.SetOwnerReference(wandb, roleBinding, client.Scheme()); err != nil { + return fmt.Errorf("failed to set owner reference on RoleBinding: %w", err) + } + + existingRoleBinding := &v4.RoleBinding{} + err := client.Get(ctx, types.NamespacedName{Name: serviceAccountName, Namespace: wandb.Namespace}, existingRoleBinding) + if err != nil { + if errors.IsNotFound(err) { + log.Info("Creating RoleBinding", "name", serviceAccountName, "namespace", wandb.Namespace) + if err := client.Create(ctx, roleBinding); err != nil { + return fmt.Errorf("failed to create RoleBinding: %w", err) + } + return nil + } + return fmt.Errorf("failed to get RoleBinding: %w", err) + } + + // Update existing rolebinding + existingRoleBinding.RoleRef = roleBinding.RoleRef + existingRoleBinding.Subjects = roleBinding.Subjects + existingRoleBinding.Labels = roleBinding.Labels + log.Info("Updating RoleBinding", "name", serviceAccountName, "namespace", wandb.Namespace) + if err := client.Update(ctx, existingRoleBinding); err != nil { + return fmt.Errorf("failed to update RoleBinding: %w", err) + } + + return nil +} + +// createOrUpdateOIDCDiscoveryClusterRoleBinding creates or updates the ClusterRoleBinding +// for OIDC discovery. This is required for JWT token validation between W&B services. +// Returns error if creation fails, but this is non-fatal for reconciliation. +func createOrUpdateOIDCDiscoveryClusterRoleBinding( + ctx context.Context, + client client.Client, + wandb *v2.WeightsAndBiases, +) error { + log := controllerruntime.LoggerFrom(ctx) + + clusterRoleBindingName := fmt.Sprintf("%s-oidc-discovery", wandb.Name) + + clusterRoleBinding := &v4.ClusterRoleBinding{ + ObjectMeta: v3.ObjectMeta{ + Name: clusterRoleBindingName, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + "app.kubernetes.io/part-of": "wandb", + }, + }, + RoleRef: v4.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "system:service-account-issuer-discovery", + }, + Subjects: []v4.Subject{ + { + APIGroup: "rbac.authorization.k8s.io", + Kind: "Group", + Name: "system:unauthenticated", + }, + }, + } + + // Note: ClusterRoleBinding cannot have ownerReferences to namespaced resources + // It will be cleaned up manually or left as cluster-scoped resource + + existingClusterRoleBinding := &v4.ClusterRoleBinding{} + err := client.Get(ctx, types.NamespacedName{Name: clusterRoleBindingName}, existingClusterRoleBinding) + if err != nil { + if errors.IsNotFound(err) { + log.Info("Creating ClusterRoleBinding for OIDC discovery", "name", clusterRoleBindingName) + if err := client.Create(ctx, clusterRoleBinding); err != nil { + return fmt.Errorf("failed to create ClusterRoleBinding: %w", err) + } + return nil + } + return fmt.Errorf("failed to get ClusterRoleBinding: %w", err) + } + + // Update existing clusterrolebinding + existingClusterRoleBinding.RoleRef = clusterRoleBinding.RoleRef + existingClusterRoleBinding.Subjects = clusterRoleBinding.Subjects + existingClusterRoleBinding.Labels = clusterRoleBinding.Labels + log.Info("Updating ClusterRoleBinding for OIDC discovery", "name", clusterRoleBindingName) + if err := client.Update(ctx, existingClusterRoleBinding); err != nil { + return fmt.Errorf("failed to update ClusterRoleBinding: %w", err) + } + + return nil +} diff --git a/internal/controller/reconciler/reconcile_v2.go b/internal/controller/reconciler/reconcile_v2.go new file mode 100644 index 00000000..e541c36a --- /dev/null +++ b/internal/controller/reconciler/reconcile_v2.go @@ -0,0 +1,1544 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + "strings" + "time" + + "github.com/samber/lo" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/ctrlqueue" + "github.com/wandb/operator/internal/logx" + wmetrics "github.com/wandb/operator/internal/metrics" + oputils "github.com/wandb/operator/pkg/utils" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + apiErrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +const CleanupFinalizer = "wandb.apps.wandb.com/cleanup" + +var defaultRequeueMinutes = 1 +var defaultRequeueDuration = time.Duration(defaultRequeueMinutes) * time.Minute + +var managedWorkloadTelemetryApplications = map[string]struct{}{ + "api": {}, + "executor": {}, + "filemeta": {}, + "filestream": {}, + "flat-run-fields-updater": {}, + "glue": {}, + "metric-observer": {}, + "parquet": {}, +} + +var managedWorkloadStatsdApplications = map[string]struct{}{ + "api": {}, + "executor": {}, + "filemeta": {}, + "filestream": {}, + "flat-run-fields-updater": {}, + "glue": {}, + "metric-observer": {}, + "parquet": {}, +} + +var managedWorkloadDatadogApplications = map[string]struct{}{ + "anaconda2": {}, + "weave-trace": {}, + "weave-trace-worker": {}, + "weave-trace-evaluate-model-worker": {}, +} + +var managedWorkloadTelemetryEnvVars = []serverManifest.EnvVar{ + { + Name: "OTEL_EXPORTER_OTLP_PROTOCOL", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "protocol"}, + }, + }, + { + Name: "OTEL_TRACES_EXPORTER", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "tracesExporter"}, + }, + }, + { + Name: "OTEL_METRICS_EXPORTER", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "metricsExporter"}, + }, + }, + { + Name: "OTEL_LOGS_EXPORTER", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "logsExporter"}, + }, + }, + { + Name: "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "metricsEndpoint"}, + }, + }, + { + Name: "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "logsEndpoint"}, + }, + }, + { + Name: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "tracesEndpoint"}, + }, + }, + { + Name: "OTEL_SERVICE_NAME", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "serviceName"}, + }, + }, + { + Name: "OTEL_RESOURCE_ATTRIBUTES", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "resourceAttributes"}, + }, + }, + { + Name: "GORILLA_TRACER", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "gorillaTracer"}, + }, + }, +} + +var managedWorkloadStatsdEnvVars = []serverManifest.EnvVar{ + { + Name: "GORILLA_STATSD_ADDRESS", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "statsdAddress"}, + }, + }, +} + +var managedWorkloadDatadogEnvVars = []serverManifest.EnvVar{ + { + Name: "DD_TRACE_AGENT_URL", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "datadogTraceAgentURL"}, + }, + }, + { + Name: "DD_AGENT_HOST", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "datadogTraceAgentHost"}, + }, + }, + { + Name: "DD_TRACE_AGENT_PORT", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "datadogTraceAgentPort"}, + }, + }, +} + +type finalizerFunc func(context.Context, ctrlClient.Client, *apiv2.WeightsAndBiases) error + +func runRetentionFinalizer( + ctx context.Context, + client ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, + infraSpec apiv2.ManagedInfraSpec, + purgeFn finalizerFunc, + detachFn finalizerFunc, +) error { + switch wandb.GetRetentionPolicy(infraSpec).OnDelete { + case apiv2.PurgeOnDelete: + return purgeFn(ctx, client, wandb) + case apiv2.DetachOnDelete: + return detachFn(ctx, client, wandb) + } + return nil +} + +// Reconcile for V2 of WandB as the assumed object +func Reconcile( + ctx context.Context, + client ctrlClient.Client, + recorder record.EventRecorder, + wandb *apiv2.WeightsAndBiases, + telemetryConfig TelemetryRuntimeConfig, +) (ctrl.Result, error) { + ctx, log := logx.WithSlog(ctx, logx.ReconcileInfraV2) + + var err error + + var errorCount int + + wandb.Status.TelemetryStatus = summarizeTelemetryInfraStatus(ctx, client, telemetryConfig) + + ///////////////////////// + // Retention Finalizer + + isFlaggedForDeletion := !wandb.GetDeletionTimestamp().IsZero() + + // ensure finalizer if not present + if !isFlaggedForDeletion && !ctrlqueue.ContainsString(wandb.GetFinalizers(), CleanupFinalizer) { + wandb.SetFinalizers(append(wandb.GetFinalizers(), CleanupFinalizer)) + if err := client.Update(ctx, wandb); err != nil { + log.Error(fmt.Sprintf("Failed to add finalizer '%s'", CleanupFinalizer), logx.ErrAttr(err)) + return ctrl.Result{}, err + } + } + + // if deleting and handle cleanup or preservation of config and data + if isFlaggedForDeletion && !wandb.GetDeletionTimestamp().IsZero() { + if ctrlqueue.ContainsString(wandb.GetFinalizers(), CleanupFinalizer) { + + // Multi-instance infra: the per-type retention dispatcher applies the + // configured policy to each managed or external instance. + for key, spec := range wandb.Spec.ObjectStore { + if err = runObjectStoreRetentionFinalizer(ctx, client, wandb, key, spec); err != nil { + return ctrl.Result{}, err + } + } + for key, spec := range wandb.Spec.MySQL { + if err = runMysqlRetentionFinalizer(ctx, client, wandb, key, spec); err != nil { + return ctrl.Result{}, err + } + } + for key, spec := range wandb.Spec.Redis { + if err = runRedisRetentionFinalizer(ctx, client, wandb, key, spec); err != nil { + return ctrl.Result{}, err + } + } + for key, spec := range wandb.Spec.ClickHouse { + if err = runClickHouseRetentionFinalizer(ctx, client, wandb, key, spec); err != nil { + return ctrl.Result{}, err + } + } + // Kafka remains single-instance. + if wandb.Spec.Kafka.ManagedKafka != nil { + if err = runRetentionFinalizer(ctx, client, wandb, wandb.Spec.Kafka.ManagedKafka.ManagedInfraSpec, kafkaPurgeFinalizer, kafkaDetachFinalizer); err != nil { + return ctrl.Result{}, err + } + } + if err = deleteInfraHTTPRoutes(ctx, client, wandb); err != nil { + return ctrl.Result{}, err + } + if wandb.Spec.Networking.Mode == apiv2.NetworkingModeIngress { + if err = deleteConsolidatedIngress(ctx, client, wandb); err != nil { + return ctrl.Result{}, err + } + } + if wandb.Spec.Networking.Mode == apiv2.NetworkingModeGatewayAPI && + wandb.Spec.Networking.GatewayAPI != nil && + wandb.Spec.Networking.GatewayAPI.Gateway.Managed { + if err = deleteGateway(ctx, client, wandb); err != nil { + return ctrl.Result{}, err + } + } + + controllerutil.RemoveFinalizer(wandb, CleanupFinalizer) + if err := client.Update(ctx, wandb); err != nil { + log.Error("Failed to remove finalizer '%s'", logx.ErrAttr(err)) + return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil + } + } + // continue post-finalizer logic in a future pass of the reconciliation loop + return ctrl.Result{}, nil + } + + ///////////////////////// + // Migrate legacy v1 conversion annotations into typed spec fields + if res, migErr := migrateLegacyAnnotations(ctx, client, wandb); migErr != nil || res.RequeueAfter > 0 { + return res, migErr + } + + ///////////////////////// + // Fetch manifest early so infra sizing can be applied before provisioning + manifest, err := serverManifest.GetServerManifest(ctx, wandb.Spec.Wandb.ManifestRepository, wandb.Spec.Wandb.Version) + if err != nil { + return ctrl.Result{}, err + } + + // Override features from CR spec if present + for key, enabled := range wandb.Spec.Wandb.Features { + manifest.Features[key] = enabled + } + + // Apply manifest-derived infra sizing before provisioning + ApplyInfraSizing(wandb, manifest) + + ///////////////////////// + // Write Infra State + redisConditions := redisWriteState(ctx, client, wandb, manifest) + mysqlConditions := mysqlWriteState(ctx, client, wandb, manifest) + objectStoreConditions, objectStoreConnection := objectStoreWriteState(ctx, client, wandb, manifest) + kafkaConditions := kafkaWriteState(ctx, client, wandb, manifest) + clickHouseConditions := clickHouseWriteState(ctx, client, wandb, manifest) + + ///////////////////////// + // Read Infra State + redisConditions, redisInfraConn := redisReadState(ctx, client, wandb, redisConditions) + mysqlConditions, mysqlInfraConn := mysqlReadState(ctx, client, wandb, mysqlConditions) + kafkaConditions, kafkaInfraConn := kafkaReadState(ctx, client, wandb, kafkaConditions) + objectStoreConditions = objectStoreReadState(ctx, client, wandb, objectStoreConditions) + clickHouseConditions, clickHouseInfraConn := clickHouseReadState(ctx, client, wandb, clickHouseConditions) + + ///////////////////////// + // WandB Status Inference + var res ctrl.Result + var ctrlResults []ctrl.Result + + if res, err = redisInferStatus(ctx, client, recorder, wandb, redisConditions, redisInfraConn); err != nil { + errorCount++ + } + ctrlResults = append(ctrlResults, res) + + if res, err = mysqlInferStatus(ctx, client, recorder, wandb, mysqlConditions, mysqlInfraConn); err != nil { + errorCount++ + } + ctrlResults = append(ctrlResults, res) + + if res, err = kafkaInferStatus(ctx, client, recorder, wandb, kafkaConditions, kafkaInfraConn); err != nil { + errorCount++ + } + ctrlResults = append(ctrlResults, res) + + if res, err = objectStoreInferStatus(ctx, client, recorder, wandb, objectStoreConditions, objectStoreConnection); err != nil { + errorCount++ + } + ctrlResults = append(ctrlResults, res) + + if res, err = clickHouseInferStatus(ctx, client, recorder, wandb, clickHouseConditions, clickHouseInfraConn); err != nil { + errorCount++ + } + ctrlResults = append(ctrlResults, res) + + if err = inferState(ctx, client, wandb); err != nil { + errorCount++ + } + + if errorCount > 0 { + return ctrl.Result{}, errors.New("infra state update errors") + } + + if err := reconcileTelemetryConnectionSecret(ctx, client, wandb, telemetryConfig); err != nil { + log.Error("failed to reconcile telemetry connection secret", logx.ErrAttr(err)) + return ctrl.Result{}, err + } + + redisReady := redisAllReady(wandb) + mysqlReady := mysqlAllReady(wandb) + kafkaReady := wandb.Status.KafkaStatus.Ready + objectStoreReady := objectStoreAllReady(wandb) + clickHouseReady := clickHouseAllReady(wandb) + + if !redisReady || !mysqlReady || !kafkaReady || !objectStoreReady || !clickHouseReady { + log := ctrl.LoggerFrom(ctx) + log.Info("Infra not ready in V2.Reconcile", + "redis", redisReady, "mysql", mysqlReady, "kafka", kafkaReady, "objectStore", objectStoreReady, "clickhouse", clickHouseReady) + return ctrl.Result{RequeueAfter: defaultRequeueDuration}, nil + } + + res, err = ReconcileWandbManifest(ctx, client, wandb, manifest, telemetryConfig) + // send up the manifest error for now + if err != nil { + return res, err + } + ctrlResults = append(ctrlResults, res) + + return consolidateResults(ctrlResults), nil +} + +func consolidateResults(results []ctrl.Result) ctrl.Result { + durations := lo.Filter( + lo.Map(results, func(r ctrl.Result, _ int) time.Duration { return r.RequeueAfter }), + func(d time.Duration, _ int) bool { return d > 0 }, + ) + // if there are no non-zero durations + if len(durations) == 0 { + return ctrl.Result{} + } + + return ctrl.Result{ + RequeueAfter: lo.Min(durations), + } +} + +func ReconcileWandbManifest( + ctx context.Context, + client ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, + manifest serverManifest.Manifest, + telemetryConfig TelemetryRuntimeConfig, +) (ctrl.Result, error) { + // Reconcile Wandb Manifest + logger := ctrl.LoggerFrom(ctx).WithName("reconcileWandbManifest") + logger.Info("Reconciling Wandb Manifest", "name", wandb.Name) + var result ctrl.Result + var err error + + statusBefore := wandb.DeepCopy().Status + + redisReady := redisAllReady(wandb) + mysqlReady := mysqlAllReady(wandb) + kafkaReady := wandb.Status.KafkaStatus.Ready + objectStoreReady := objectStoreAllReady(wandb) + clickHouseReady := clickHouseAllReady(wandb) + + if !redisReady || !mysqlReady || !kafkaReady || !objectStoreReady || !clickHouseReady { + logger.Info("Infra components not ready yet, requeuing for reconciliation", + "redis", redisReady, "moco", mysqlReady, "kafka", kafkaReady, "objectStore", objectStoreReady, "clickhouse", clickHouseReady) + return ctrl.Result{RequeueAfter: defaultRequeueDuration}, nil + } + + logger.Info("Manifest Features", "features", manifest.Features) + + validateLegacyOverrides(ctx, wandb, manifest) + + result, err = generateSecrets(ctx, client, wandb, manifest) + if err != nil { + return result, err + } + + result, err = createKafkaTopics(ctx, client, wandb, manifest) + if err != nil { + return result, err + } + + result, err = runMysqlInitJob(ctx, client, wandb, manifest) + if err != nil { + return result, err + } + + if !allMysqlInitSucceeded(wandb) { + logger.Info("Mysql init not yet successful") + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil + } + + serviceAccountName := wandb.Spec.Wandb.ServiceAccount.ServiceAccountName + + if wandb.Spec.Wandb.ServiceAccount.Create != nil && *wandb.Spec.Wandb.ServiceAccount.Create { + if err := createOrUpdateServiceAccount(ctx, client, wandb, serviceAccountName); err != nil { + logger.Error(err, "Failed to create/update ServiceAccount") + return ctrl.Result{}, err + } + } + + if err := createOrUpdateRole(ctx, client, wandb, serviceAccountName); err != nil { + logger.Error(err, "Failed to create/update Role for service account") + return ctrl.Result{}, err + } + + if err := createOrUpdateRoleBinding(ctx, client, wandb, serviceAccountName); err != nil { + logger.Error(err, "Failed to create/update RoleBinding for service account") + return ctrl.Result{}, err + } + + if err := cleanupNetworkingModeResources(ctx, client, wandb); err != nil { + logger.Error(err, "Failed to clean up stale networking resources") + return ctrl.Result{}, err + } + resetInactiveNetworkingStatus(wandb) + + if err := reconcileCustomCACerts(ctx, client, wandb); err != nil { + logger.Error(err, "Failed to reconcile custom CA certificates") + return ctrl.Result{}, err + } + + if wandb.Spec.Networking.Mode == apiv2.NetworkingModeGatewayAPI { + wandb.Status.GatewayStatus = nil + if err := reconcileGateway(ctx, client, wandb); err != nil { + logger.Error(err, "Failed to reconcile Gateway") + return ctrl.Result{}, err + } + } + + result, err = runMigrations(ctx, client, wandb, manifest) + if err != nil { + return result, err + } + + if !wandb.Status.Wandb.Migration.Ready { + logger.Info("Migration not yet successful for version", "version", wandb.Spec.Wandb.Version, "reason", wandb.Status.Wandb.Migration.Reason) + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil + } + + result, err = reconcileApplications(ctx, client, wandb, manifest, telemetryConfig) + if err != nil { + return result, err + } + + // Gate on live Deployment readiness, not status.wandb.applications: the + // copied status map can be a stale snapshot (it only refreshes when this + // reconciler runs), and a frozen mid-rollout entry would block cleanup forever. + if healthy, notReady := deploymentsHealthy(ctx, client, wandb.Namespace, buildDesiredAppNames(manifest)); healthy { + if err := cleanupLegacyV1Deployments(ctx, client, wandb); err != nil { + logger.Error(err, "Failed to clean up legacy v1 deployments") + return ctrl.Result{}, err + } + } else { + logger.Info("Deferring legacy v1 deployment cleanup until all application Deployments are ready", + "notReady", notReady) + } + + if wandb.Spec.Networking.Mode == apiv2.NetworkingModeGatewayAPI { + if err := reconcileInfraHTTPRoutes(ctx, client, wandb, manifest); err != nil { + logger.Error(err, "Failed to reconcile infra HTTPRoutes") + return ctrl.Result{}, err + } + } + + if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func reconcileApplications( + ctx context.Context, + client ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, + manifest serverManifest.Manifest, + telemetryConfig TelemetryRuntimeConfig, +) (ctrl.Result, error) { + logger := logx.GetSlog(ctx) + logger.Info("Reconciling applications") + serviceAccountName := wandb.Spec.Wandb.ServiceAccount.ServiceAccountName + + if wandb.Spec.Wandb.InternalServiceAuth.Enabled != nil && *wandb.Spec.Wandb.InternalServiceAuth.Enabled { + if err := createOrUpdateOIDCDiscoveryClusterRoleBinding(ctx, client, wandb); err != nil { + logger.Error("Failed to create ClusterRoleBinding for OIDC discovery. "+ + "This is required for JWT token validation between W&B services. "+ + "Either grant the operator ClusterRoleBinding permissions, or manually create the ClusterRoleBinding. "+ + "W&B will continue starting, but JWT authentication will fail until this is resolved.", "err", err) + // Non-fatal: continue reconciliation even if ClusterRoleBinding creation fails + } + } + + desiredAppNames := buildDesiredAppNames(manifest) + + for _, app := range sortedManifestApplications(manifest) { + // If the application is gated behind features, only install it when + // at least one of those features is enabled in the manifest. + if len(app.Features) > 0 && !manifest.FeaturesEnabled(app.Features) { + continue + } + + app = applyWandbProbeDefaults(app, wandb.Spec.Wandb.Probes) + + envVars, err := resolveEnvvars(ctx, client, wandb, manifest, app.CommonEnvs, app.Env) + if err != nil { + return ctrl.Result{}, err + } + envVars, err = injectManagedWorkloadTelemetryEnvvars(ctx, client, wandb, manifest, app, envVars, telemetryConfig) + if err != nil { + return ctrl.Result{}, err + } + envVars = applyWorkloadTelemetryDefaults(envVars, app.Name) + + volumes, volumeMounts, err := resolveVolumeMounts(ctx, manifest, app.CommonVolumeMounts, app.VolumeMounts) + if err != nil { + return ctrl.Result{}, err + } + + // First, resolve any inline files and JWT token volumes at the Application level + // so that volumeMounts/volumes are ready before constructing containers. + if len(app.Files) > 0 { + volumes, volumeMounts, err = resolveInlineFiles(ctx, client, wandb, app, volumes, volumeMounts) + if err != nil { + return ctrl.Result{}, err + } + } + if len(app.JWTTokens) > 0 { + // resolveJWTTokens appends mounts to the given container, but also returns volumes. + // We only use the returned volumes here, consistent with previous behavior. + volumes, volumeMounts = resolveJWTTokens(app, volumes, volumeMounts) + } + + var caChecksum string + envVars, volumes, volumeMounts, caChecksum, err = applyCustomCACertsToWorkload(ctx, client, wandb, envVars, volumes, volumeMounts) + if err != nil { + return ctrl.Result{}, err + } + + // Applied last so legacy overrides beat manifest and injected env, as in v1. + envVars = applyLegacyOverrideEnv(ctx, wandb, app.Name, envVars) + + containers := resolveContainers(app, wandb, envVars, volumeMounts) + + initContainers := resolveInitContainers(app, wandb, envVars, volumeMounts) + + application := &apiv2.Application{} + err = client.Get(ctx, types.NamespacedName{Name: app.Name, Namespace: wandb.Namespace}, application) + before := application.DeepCopy() + if err != nil { + if apiErrors.IsNotFound(err) { + application.SetName(app.Name) + application.SetNamespace(wandb.Namespace) + } else { + return ctrl.Result{}, err + } + } + + application.Spec.Kind = "Deployment" + application.Spec.PodTemplate.Spec.Containers = containers + // Replace volumes entirely on each reconcile to avoid accumulating duplicates + // across updates (e.g., duplicate "files-inline" volume names). + application.Spec.PodTemplate.Spec.Volumes = volumes + application.Spec.PodTemplate.Spec.InitContainers = initContainers + application.Spec.PodTemplate.Spec.SecurityContext = resolvePodSecurityContext() + application.Spec.PodTemplate.Spec.Affinity = wandb.Spec.Affinity + application.Spec.PodTemplate.Spec.Tolerations = *wandb.Spec.Tolerations + setCustomCACertsChecksumAnnotation(&application.Spec.PodTemplate, caChecksum) + + application.Spec.HpaTemplate = ResolveAutoscaling(app, wandb) + + // Set shared service account for all W&B applications + application.Spec.PodTemplate.Spec.ServiceAccountName = serviceAccountName + + // Reconcile Service ports: fully replace the ServiceTemplate ports with + // the ports declared in the manifest for this app. This ensures that any + // change to port numbers, names, or protocols is propagated on each + // reconcile. If no service ports are declared, clear the ServiceTemplate. + if app.Service != nil && len(app.Service.Ports) > 0 { + // Copy + normalize: the CRD schema defaults ports[].protocol, so an + // un-normalized template never round-trips equal and the update gate + // below would fire on every reconcile, churning the Application. + ports := make([]corev1.ServicePort, len(app.Service.Ports)) + copy(ports, app.Service.Ports) + common.NormalizeServicePorts(ports) + application.Spec.ServiceTemplate = &corev1.ServiceSpec{ + Type: app.Service.Type, + Ports: ports, + } + } else { + // No service declared in manifest; ensure we clear any previous template + application.Spec.ServiceTemplate = nil + } + + if wandb.Spec.Networking.Mode == apiv2.NetworkingModeGatewayAPI && app.Ingress != nil && + wandb.Status.GatewayStatus != nil && wandb.Status.GatewayStatus.GatewayRef != nil { + application.Spec.HTTPRouteTemplate = buildHTTPRouteTemplate(wandb, app) + } else { + application.Spec.HTTPRouteTemplate = nil + } + + // A plain owner ref (not a controller ref) so multiple CRs can share a + // namespace; the parent's Owns(Application) watch uses MatchEveryOwner + // to still enqueue on app status changes. + err = controllerutil.SetOwnerReference(wandb, application, client.Scheme()) + if err != nil { + return ctrl.Result{}, err + } + + if application.CreationTimestamp.IsZero() { + if err = client.Create(ctx, application); err != nil { + return ctrl.Result{}, err + } + } else if !applicationManagedFieldsEqual(before, application) { + if err = client.Update(ctx, application); err != nil { + return ctrl.Result{}, err + } + } + + wmetrics.SetApplicationInfo(app.Name, wandb.Namespace, app.Image.Repository, app.Image.Tag, app.Image.Digest) + + wandb.Status.Wandb.Applications[app.Name] = application.Status + } + + existingApps := &apiv2.ApplicationList{} + if err := client.List(ctx, existingApps, ctrlClient.InNamespace(wandb.Namespace)); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to list existing applications: %w", err) + } + + for _, app := range existingApps.Items { + if !isOwnedBy(&app, wandb) { + continue + } + + // Infra-managed Applications (e.g. managed Kafka/etcd) carry a component + // label and are owned by their dedicated infra reconcilers, not the + // server manifest. Skip them so manifest-driven pruning never deletes + // them, which would otherwise cause a delete/recreate loop. + if _, ok := app.Labels[common.WandbComponentLabel]; ok { + continue + } + + if !desiredAppNames[app.Name] { + logger.Info("Deleting application no longer in manifest or disabled by feature", "application", app.Name) + if err := client.Delete(ctx, &app); err != nil && !apiErrors.IsNotFound(err) { + return ctrl.Result{}, fmt.Errorf("failed to delete application %s: %w", app.Name, err) + } + delete(wandb.Status.Wandb.Applications, app.Name) + wmetrics.DeleteApplicationInfo(app.Name, wandb.Namespace) + } + } + + if wandb.Spec.Networking.Mode == apiv2.NetworkingModeIngress { + wandb.Status.IngressStatus = nil + if err := reconcileConsolidatedIngress(ctx, client, wandb, manifest); err != nil { + logger.Error("Failed to reconcile consolidated Ingress", "err", err) + return ctrl.Result{}, err + } + } + + hostname, err := url.Parse(wandb.Spec.Wandb.Hostname) + if err != nil { + logger.Error("Failed to parse provided hostname", "hostname", wandb.Spec.Wandb.Hostname, "err", err) + } else { + if wandb.Spec.Networking.Mode == apiv2.NetworkingModeNone { + // Only override with NodePort if user didn't specify a port in the hostname + if manifest.FeaturesEnabled([]string{"proxy"}) && hostname.Port() == "" { + proxyService := &corev1.Service{} + proxyServiceName := fmt.Sprintf("%s-%s", wandb.Name, "nginx-proxy") + err := client.Get(ctx, types.NamespacedName{Name: proxyServiceName, Namespace: wandb.Namespace}, proxyService) + if err != nil { + logger.Error("Failed to get proxy service", "service", proxyServiceName, "err", err) + } else { + if len(proxyService.Spec.Ports) == 0 { + logger.Error("Proxy service has no ports", "service", proxyServiceName) + } else { + nodePort := proxyService.Spec.Ports[0].NodePort + hostname.Host = fmt.Sprintf("%s:%d", hostname.Hostname(), nodePort) + } + } + } + } + + if wandb.Status.Wandb.Hostname != hostname.String() { + wandb.Status.Wandb.Hostname = hostname.String() + } + } + + // Every application now carries this generation's spec, so consumers can + // gate on observedGeneration == generation plus workload rollout. Earlier + // exits (infra, mysql-init, migrations) must not advance it: their specs + // haven't reached the workloads yet. + wandb.Status.ObservedGeneration = wandb.GetGeneration() + + return ctrl.Result{}, nil +} + +func applicationManagedFieldsEqual(before, after *apiv2.Application) bool { + return apiequality.Semantic.DeepEqual(before.Spec, after.Spec) && + apiequality.Semantic.DeepEqual(before.Labels, after.Labels) && + apiequality.Semantic.DeepEqual(before.Annotations, after.Annotations) && + apiequality.Semantic.DeepEqual(before.OwnerReferences, after.OwnerReferences) +} + +func buildHTTPRouteTemplate(wandb *apiv2.WeightsAndBiases, app serverManifest.Application) *apiv2.HTTPRouteTemplateSpec { + gwConfig := wandb.Spec.Networking.GatewayAPI + + ref := wandb.Status.GatewayStatus.GatewayRef + parentRef := gatewayv1.ParentReference{ + Name: gatewayv1.ObjectName(ref.Name), + } + if ref.Namespace != "" && ref.Namespace != wandb.Namespace { + ns := gatewayv1.Namespace(ref.Namespace) + parentRef.Namespace = &ns + } + if gwConfig.ListenerName != nil { + sectionName := gatewayv1.SectionName(*gwConfig.ListenerName) + parentRef.SectionName = §ionName + } + + hostname := parseHostname(wandb.Spec.Wandb.Hostname) + hostnames := []gatewayv1.Hostname{gatewayv1.Hostname(hostname)} + for _, h := range wandb.Spec.Wandb.AdditionalHostnames { + hostnames = append(hostnames, gatewayv1.Hostname(h)) + } + + var paths []string + var pathType string + if app.Ingress != nil { + paths = app.Ingress.Paths + pathType = app.Ingress.PathType + } + + return &apiv2.HTTPRouteTemplateSpec{ + ParentRefs: []gatewayv1.ParentReference{parentRef}, + Hostnames: hostnames, + Paths: paths, + PathType: pathType, + ServicePort: resolveHTTPRouteServicePort(app), + } +} + +func resolveHTTPRouteServicePort(app serverManifest.Application) *gatewayv1.PortNumber { + if app.Ingress != nil && app.Ingress.ServicePort != "" { + port := intstr.Parse(app.Ingress.ServicePort) + if port.Type == intstr.Int { + p := gatewayv1.PortNumber(port.IntVal) + return &p + } + if port.Type == intstr.String && app.Service != nil { + for _, servicePort := range app.Service.Ports { + if servicePort.Name == port.StrVal { + p := gatewayv1.PortNumber(servicePort.Port) + return &p + } + } + } + } + return nil +} + +func applyWorkloadTelemetryDefaults(envVars []corev1.EnvVar, applicationName string) []corev1.EnvVar { + if applicationName == "" || !hasWorkloadTelemetryConfig(envVars) { + return envVars + } + + serviceNameIndex := -1 + for i, envVar := range envVars { + if envVar.Name != "OTEL_SERVICE_NAME" { + continue + } + serviceNameIndex = i + if envVar.Value != "" { + return envVars + } + break + } + + serviceNameEnv := corev1.EnvVar{ + Name: "OTEL_SERVICE_NAME", + Value: applicationName, + } + + if serviceNameIndex == -1 { + return append(envVars, serviceNameEnv) + } + + envVars[serviceNameIndex] = serviceNameEnv + return envVars +} + +func injectManagedWorkloadTelemetryEnvvars( + ctx context.Context, + client ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, + manifest serverManifest.Manifest, + app serverManifest.Application, + envVars []corev1.EnvVar, + telemetryConfig TelemetryRuntimeConfig, +) ([]corev1.EnvVar, error) { + + if !telemetryConfig.Enabled { + return envVars, nil + } + + if shouldInjectManagedWorkloadTelemetry(app.Name) { + var err error + envVars, err = appendResolvedManagedTelemetryEnvvars(ctx, client, wandb, manifest, envVars, managedWorkloadTelemetryEnvVars) + if err != nil { + return nil, err + } + } + + if shouldInjectManagedWorkloadStatsd(app.Name) { + var err error + envVars, err = appendResolvedManagedTelemetryEnvvars(ctx, client, wandb, manifest, envVars, managedWorkloadStatsdEnvVars) + if err != nil { + return nil, err + } + } + + if shouldInjectManagedWorkloadDatadog(app.Name) { + var err error + envVars, err = appendResolvedManagedTelemetryEnvvars(ctx, client, wandb, manifest, envVars, managedWorkloadDatadogEnvVars) + if err != nil { + return nil, err + } + envVars = appendMissingEnvVars(envVars, []corev1.EnvVar{{Name: "DD_SERVICE", Value: app.Name}}) + } + + return envVars, nil +} + +func appendResolvedManagedTelemetryEnvvars( + ctx context.Context, + client ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, + manifest serverManifest.Manifest, + envVars []corev1.EnvVar, + managedTelemetryEnvVars []serverManifest.EnvVar, +) ([]corev1.EnvVar, error) { + telemetryEnvVars, err := resolveEnvvars(ctx, client, wandb, manifest, nil, managedTelemetryEnvVars) + if err != nil { + return nil, err + } + return appendMissingEnvVars(envVars, telemetryEnvVars), nil +} + +func shouldInjectManagedWorkloadTelemetry(appName string) bool { + _, ok := managedWorkloadTelemetryApplications[appName] + return ok +} + +func shouldInjectManagedWorkloadStatsd(appName string) bool { + _, ok := managedWorkloadStatsdApplications[appName] + return ok +} + +func shouldInjectManagedWorkloadDatadog(appName string) bool { + _, ok := managedWorkloadDatadogApplications[appName] + return ok +} + +func appendMissingEnvVars(existing []corev1.EnvVar, additions []corev1.EnvVar) []corev1.EnvVar { + seen := make(map[string]struct{}, len(existing)) + for _, envVar := range existing { + seen[envVar.Name] = struct{}{} + } + + for _, envVar := range additions { + if _, ok := seen[envVar.Name]; ok { + continue + } + existing = append(existing, envVar) + seen[envVar.Name] = struct{}{} + } + + return existing +} + +func hasWorkloadTelemetryConfig(envVars []corev1.EnvVar) bool { + for _, envVar := range envVars { + switch envVar.Name { + case "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_METRICS_EXPORTER", + "OTEL_LOGS_EXPORTER", + "OTEL_TRACES_EXPORTER", + "OTEL_SERVICE_NAME", + "GORILLA_TRACER": + return true + } + } + return false +} + +func resolveJWTTokens(app serverManifest.Application, volumes []corev1.Volume, volumeMounts []corev1.VolumeMount) ([]corev1.Volume, []corev1.VolumeMount) { + for _, jwtToken := range app.JWTTokens { + var volume corev1.Volume + volumeName := fmt.Sprintf("%s-%s", app.Name, jwtToken.Name) + + // Create volume based on source type + switch { + case jwtToken.Source.KubernetesServiceAccount != nil: + // Projected volume with service account token + expirationSeconds := jwtToken.Source.KubernetesServiceAccount.ExpirationSeconds + if expirationSeconds == 0 { + expirationSeconds = 3607 // Default 1 hour + 7 seconds + } + volume = corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ + { + ServiceAccountToken: &corev1.ServiceAccountTokenProjection{ + Audience: jwtToken.Source.KubernetesServiceAccount.Audience, + Path: "token", + ExpirationSeconds: &expirationSeconds, + }, + }, + }, + }, + }, + } + + case jwtToken.Source.SecretRef != nil: + // Secret volume + key := jwtToken.Source.SecretRef.Key + if key == "" { + key = "token" // Default key name + } + volume = corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: jwtToken.Source.SecretRef.Name, + Items: []corev1.KeyToPath{ + { + Key: key, + Path: "token", + }, + }, + }, + }, + } + + case jwtToken.Source.CSIProvider != nil: + // CSI volume + volume = corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + CSI: &corev1.CSIVolumeSource{ + Driver: jwtToken.Source.CSIProvider.Driver, + VolumeAttributes: jwtToken.Source.CSIProvider.Parameters, + ReadOnly: func() *bool { b := true; return &b }(), + }, + }, + } + + default: + // No valid source specified, skip this token + continue + } + + volumes = append(volumes, volume) + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: volumeName, + MountPath: jwtToken.MountPath, + ReadOnly: true, + }) + } + return volumes, volumeMounts +} + +func resolveInlineFiles(ctx context.Context, client ctrlClient.Client, wandb *apiv2.WeightsAndBiases, app serverManifest.Application, volumes []corev1.Volume, volumeMounts []corev1.VolumeMount) ([]corev1.Volume, []corev1.VolumeMount, error) { + // Collect inline files into a single operator-managed ConfigMap + inlineData := map[string]string{} + inlineCMName := fmt.Sprintf("%s-%s-files", wandb.Name, app.Name) + // Track external ConfigMap refs and create one Volume per unique ref + cmRefVolumeNames := map[string]string{} + + for _, f := range app.Files { + key := f.Name + fileName := f.FileName + if fileName == "" { + fileName = key + } + + var volName string + if f.Inline != "" { + // Accumulate into inline CM data + inlineData[key] = f.Inline + volName = "files-inline" + } else if f.ConfigMapRef != "" { + // external ConfigMap reference + if existing, ok := cmRefVolumeNames[f.ConfigMapRef]; ok { + volName = existing + } else { + volName = fmt.Sprintf("cm-%s", f.ConfigMapRef) + cmRefVolumeNames[f.ConfigMapRef] = volName + } + } else { + // neither inline nor ref provided; skip + continue + } + + // Mount each file as a single file using subPath into the specified directory + mountPath := f.MountPath + if mountPath == "" { + // require a mountPath; skip if not provided + continue + } + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: volName, + MountPath: fmt.Sprintf("%s/%s", mountPath, fileName), + SubPath: key, + ReadOnly: true, + }) + } + + // Create/update inline ConfigMap if we have any inline data + if len(inlineData) > 0 { + cm := &corev1.ConfigMap{} + cm.Namespace = wandb.Namespace + cm.Name = inlineCMName + // Try to get existing + err := client.Get(ctx, types.NamespacedName{Name: cm.Name, Namespace: cm.Namespace}, cm) + if err != nil { + if apiErrors.IsNotFound(err) { + cm.Data = inlineData + if err := controllerutil.SetOwnerReference(wandb, cm, client.Scheme()); err != nil { + return volumes, volumeMounts, err + } + if err := client.Create(ctx, cm); err != nil { + return volumes, volumeMounts, err + } + } else { + return volumes, volumeMounts, err + } + } else { + // Update data if changed + if cm.Data == nil { + cm.Data = map[string]string{} + } + cm.Data = inlineData + if err := client.Update(ctx, cm); err != nil { + return volumes, volumeMounts, err + } + } + + // Add a volume for the inline CM + volumes = append(volumes, corev1.Volume{ + Name: "files-inline", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: inlineCMName}}, + }, + }) + } + + // Add volumes for each external ConfigMap ref + for ref, volName := range cmRefVolumeNames { + volumes = append(volumes, corev1.Volume{ + Name: volName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: ref}}, + }, + }) + } + + return volumes, volumeMounts, nil +} + +func runMigrations(ctx context.Context, client ctrlClient.Client, wandb *apiv2.WeightsAndBiases, manifest serverManifest.Manifest) (ctrl.Result, error) { + statusBefore := wandb.DeepCopy().Status + version := wandb.Spec.Wandb.Version + + if wandb.Status.Wandb.Migration.Ready && wandb.Status.Wandb.Migration.Version == version { + for name := range manifest.Migrations { + jobName := fmt.Sprintf("%s-%s", wandb.Name, name) + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: jobName, + Namespace: wandb.Namespace, + }, + } + propagation := metav1.DeletePropagationBackground + deleteOptions := &ctrlClient.DeleteOptions{PropagationPolicy: &propagation} + err := client.Delete(ctx, job, deleteOptions) + if err != nil { + if !apiErrors.IsNotFound(err) { + return ctrl.Result{}, fmt.Errorf("failed to delete migration job %s: %v", jobName, err) + } + } + } + return ctrl.Result{}, nil + } + + if wandb.Status.Wandb.Migration.Version != version { + wandb.Status.Wandb.Migration.Version = version + wandb.Status.Wandb.Migration.Ready = false + wandb.Status.Wandb.Migration.Reason = "Running" + wandb.Status.Wandb.Migration.Jobs = make(map[string]apiv2.MigrationJobStatus) + if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { + return ctrl.Result{}, err + } + statusBefore = wandb.DeepCopy().Status + } + + if len(manifest.Migrations) == 0 { + wandb.Status.Wandb.Migration.Ready = true + wandb.Status.Wandb.Migration.Reason = "Complete" + wandb.Status.Wandb.Migration.LastSuccessVersion = version + if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + if wandb.Status.Wandb.Migration.Jobs == nil { + wandb.Status.Wandb.Migration.Jobs = make(map[string]apiv2.MigrationJobStatus) + } + + allSucceeded := true + anyFailed := false + anyRunning := false + + for name, migrationTask := range manifest.Migrations { + jobName := fmt.Sprintf("%s-%s", wandb.Name, name) + job := &batchv1.Job{} + err := client.Get(ctx, types.NamespacedName{Name: jobName, Namespace: wandb.Namespace}, job) + + jobStatus := apiv2.MigrationJobStatus{ + Name: jobName, + } + + if err != nil && !apiErrors.IsNotFound(err) { + return ctrl.Result{}, err + } + + if apiErrors.IsNotFound(err) { + + envVars, err := resolveEnvvars(ctx, client, wandb, manifest, migrationTask.CommonEnvs, migrationTask.Env) + if err != nil { + return ctrl.Result{}, err + } + + volumes, volumeMounts, err := resolveVolumeMounts(ctx, manifest, migrationTask.CommonVolumeMounts, migrationTask.VolumeMounts) + if err != nil { + return ctrl.Result{}, err + } + + var caChecksum string + envVars, volumes, volumeMounts, caChecksum, err = applyCustomCACertsToWorkload(ctx, client, wandb, envVars, volumes, volumeMounts) + if err != nil { + return ctrl.Result{}, err + } + + // v1's global env reached job pods too (e.g. HTTP_PROXY); per-app entries don't apply here. + envVars = overrideEnvVars(ctx, envVars, wandb.Spec.Wandb.LegacyOverrides[apiv2.LegacyOverridesGlobalKey].Env) + + podTemplate := corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyOnFailure, + Containers: []corev1.Container{ + { + Name: "migrate", + Image: migrationTask.Image.GetImage(""), + Args: migrationTask.Args, + Command: migrationTask.Command, + Env: envVars, + VolumeMounts: volumeMounts, + }, + }, + Volumes: volumes, + ServiceAccountName: wandb.Spec.Wandb.ServiceAccount.ServiceAccountName, + }, + } + setCustomCACertsChecksumAnnotation(&podTemplate, caChecksum) + + job = &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: jobName, + Namespace: wandb.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + "app.kubernetes.io/component": "migration", + }, + }, + Spec: batchv1.JobSpec{ + Template: podTemplate, + }, + } + + if err := controllerutil.SetOwnerReference(wandb, job, client.Scheme()); err != nil { + return ctrl.Result{}, err + } + + if err := client.Create(ctx, job); err != nil { + return ctrl.Result{}, err + } + + jobStatus.Succeeded = false + wandb.Status.Wandb.Migration.Jobs[name] = jobStatus + wandb.Status.Wandb.Migration.Reason = "Running" + wandb.Status.Wandb.Migration.Ready = false + if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil + } + + if job.Status.Succeeded > 0 { + jobStatus.Succeeded = true + } else { + allSucceeded = false + for _, cond := range job.Status.Conditions { + if cond.Type == batchv1.JobFailed && cond.Status == corev1.ConditionTrue { + jobStatus.Failed = true + jobStatus.Message = cond.Message + anyFailed = true + break + } + } + if !jobStatus.Failed { + anyRunning = true + } + } + + wandb.Status.Wandb.Migration.Jobs[name] = jobStatus + } + + if anyFailed { + wandb.Status.Wandb.Migration.Reason = "Failed" + wandb.Status.Wandb.Migration.Ready = false + } else if anyRunning || !allSucceeded { + wandb.Status.Wandb.Migration.Reason = "Running" + wandb.Status.Wandb.Migration.Ready = false + } else if allSucceeded { + wandb.Status.Wandb.Migration.Reason = "Complete" + wandb.Status.Wandb.Migration.Ready = true + if wandb.Status.Wandb.Migration.LastSuccessVersion != version { + wandb.Status.Wandb.Migration.LastSuccessVersion = version + } + } else { + wandb.Status.Wandb.Migration.Reason = "Unknown" + wandb.Status.Wandb.Migration.Ready = false + } + + if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { + return ctrl.Result{}, err + } + + if allSucceeded { + return ctrl.Result{}, nil + } + + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil +} + +func generateSecrets(ctx context.Context, client ctrlClient.Client, wandb *apiv2.WeightsAndBiases, manifest serverManifest.Manifest) (ctrl.Result, error) { + statusBefore := wandb.DeepCopy().Status + // Ensure any manifest-declared generated secrets exist and capture their selectors in status + if wandb.Status.GeneratedSecrets == nil { + wandb.Status.GeneratedSecrets = map[string]corev1.SecretKeySelector{} + } + for _, gs := range manifest.GeneratedSecrets { + // Deterministic secret name scoped to the CR instance + // If UseExactName is true, use the exact name without prefixing + secretName := gs.Name + if !gs.UseExactName { + secretName = fmt.Sprintf("%s-%s", wandb.Name, gs.Name) + } + keyName := "key" + sec := &corev1.Secret{} + err := client.Get(ctx, types.NamespacedName{Name: secretName, Namespace: wandb.Namespace}, sec) + if err != nil { + if apiErrors.IsNotFound(err) { + // Create new secret with generated value + valueLen := gs.Length + if valueLen <= 0 { + valueLen = 32 + } + pw, err := oputils.GenerateRandomPassword(valueLen) + if err != nil { + return ctrl.Result{}, err + } + sec = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: wandb.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + "app.kubernetes.io/part-of": "wandb", + }, + }, + StringData: map[string]string{keyName: pw}, + Type: corev1.SecretTypeOpaque, + } + if err := controllerutil.SetOwnerReference(wandb, sec, client.Scheme()); err != nil { + return ctrl.Result{}, err + } + if err := client.Create(ctx, sec); err != nil { + return ctrl.Result{}, err + } + } else { + return ctrl.Result{}, err + } + } else { + // Secret exists. Ensure it has the expected key; do not overwrite existing value. + if sec.Data == nil || (sec.Data != nil && sec.Data[keyName] == nil && sec.StringData == nil) { + if sec.StringData == nil { + sec.StringData = map[string]string{} + } + // Generate a value only if missing + valueLen := gs.Length + if valueLen <= 0 { + valueLen = 32 + } + pw, err := oputils.GenerateRandomPassword(valueLen) + if err != nil { + return ctrl.Result{}, err + } + sec.StringData[keyName] = pw + if err := client.Update(ctx, sec); err != nil { + return ctrl.Result{}, err + } + } + } + // Record selector in status + wandb.Status.GeneratedSecrets[gs.Name] = corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + Key: keyName, + } + } + // Persist status after updating generated secret selectors + if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil +} + +// resolveCRField traverses a dotted field path (e.g., "spec.wandb.license") in the +// provided custom resource object and returns the raw terminal value if present. +// Typed accessors (resolveCRFieldString, resolveCRFieldSecretSelector, ...) build on +// top of this to validate and cast the result to the type they expect. +func resolveCRField(obj any, path string) (any, bool) { + if obj == nil || path == "" { + return nil, false + } + // Marshal to JSON then unmarshal into a generic map for easy traversal. + b, err := json.Marshal(obj) + if err != nil { + return nil, false + } + var m map[string]any + if err := json.Unmarshal(b, &m); err != nil { + return nil, false + } + cur := any(m) + for _, seg := range strings.Split(path, ".") { + mm, ok := cur.(map[string]any) + if !ok { + return nil, false + } + next, ok := mm[seg] + if !ok { + return nil, false + } + cur = next + } + return cur, true +} + +// resolveCRFieldString resolves a dotted field path from the provided custom resource +// object, returning the string value if present. Non-string terminal values are +// treated as not found. +func resolveCRFieldString(obj any, path string) (string, bool) { + cur, ok := resolveCRField(obj, path) + if !ok { + return "", false + } + s, ok := cur.(string) + return s, ok +} + +func resolveCRFieldSecretSelector(obj any, path string) (corev1.SecretKeySelector, bool) { + cur, ok := resolveCRField(obj, path) + if !ok { + return corev1.SecretKeySelector{}, false + } + // Re-marshal the terminal node into a SecretKeySelector so we honor the same + // json tags (name/key/optional) the CRD uses. + tb, err := json.Marshal(cur) + if err != nil { + return corev1.SecretKeySelector{}, false + } + var sel corev1.SecretKeySelector + if err := json.Unmarshal(tb, &sel); err != nil { + return corev1.SecretKeySelector{}, false + } + if sel.Name == "" || sel.Key == "" { + return corev1.SecretKeySelector{}, false + } + return sel, true +} + +// managedInstancesReady reports whether every managed instance has a ready +// status. External and absent instances are treated as ready, matching the +// pre-multi-instance behavior where only operator-managed infra gated overall +// readiness. +func managedInstancesReady[S any, T any](specs map[string]S, statuses map[string]T, isManaged func(S) bool, ready func(T) bool) bool { + for key, spec := range specs { + if isManaged(spec) && !ready(statuses[key]) { + return false + } + } + return true +} + +// allInstancesReady reports whether every instance (managed or external) has a +// ready status. +func allInstancesReady[S any, T any](specs map[string]S, statuses map[string]T, ready func(T) bool) bool { + for key := range specs { + if !ready(statuses[key]) { + return false + } + } + return true +} + +func redisAllReady(wandb *apiv2.WeightsAndBiases) bool { + return allInstancesReady(wandb.Spec.Redis, wandb.Status.RedisStatus, func(s apiv2.RedisInfraStatus) bool { return s.Ready }) +} + +func mysqlAllReady(wandb *apiv2.WeightsAndBiases) bool { + return allInstancesReady(wandb.Spec.MySQL, wandb.Status.MySQLStatus, func(s apiv2.MysqlInfraStatus) bool { return s.Ready }) +} + +func objectStoreAllReady(wandb *apiv2.WeightsAndBiases) bool { + return allInstancesReady(wandb.Spec.ObjectStore, wandb.Status.ObjectStoreStatus, func(s apiv2.ObjectStoreInfraStatus) bool { return s.Ready }) +} + +func clickHouseAllReady(wandb *apiv2.WeightsAndBiases) bool { + return allInstancesReady(wandb.Spec.ClickHouse, wandb.Status.ClickHouseStatus, func(s apiv2.ClickHouseInfraStatus) bool { return s.Ready }) +} + +func inferState( + ctx context.Context, client ctrlClient.Client, wandb *apiv2.WeightsAndBiases, +) error { + log := ctrl.LoggerFrom(ctx) + statusBefore := wandb.DeepCopy().Status + + redisOk := managedInstancesReady(wandb.Spec.Redis, wandb.Status.RedisStatus, func(s apiv2.RedisSpec) bool { return s.ManagedRedis != nil }, func(s apiv2.RedisInfraStatus) bool { return s.Ready }) + objectStoreOk := managedInstancesReady(wandb.Spec.ObjectStore, wandb.Status.ObjectStoreStatus, func(s apiv2.ObjectStoreSpec) bool { return s.ManagedObjectStore != nil }, func(s apiv2.ObjectStoreInfraStatus) bool { return s.Ready }) + mysqlOk := managedInstancesReady(wandb.Spec.MySQL, wandb.Status.MySQLStatus, func(s apiv2.MySQLSpec) bool { return s.ManagedMysql != nil }, func(s apiv2.MysqlInfraStatus) bool { return s.Ready }) + clickHouseOk := managedInstancesReady(wandb.Spec.ClickHouse, wandb.Status.ClickHouseStatus, func(s apiv2.ClickHouseSpec) bool { return s.ManagedClickHouse != nil }, func(s apiv2.ClickHouseInfraStatus) bool { return s.Ready }) + kafkaOk := wandb.Spec.Kafka.ManagedKafka == nil || wandb.Status.KafkaStatus.Ready + + if redisOk && objectStoreOk && mysqlOk && clickHouseOk && kafkaOk { + wandb.Status.Ready = true + } else { + wandb.Status.Ready = false + } + + if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { + log.Error(err, "Failed to update status") + return err + } + return nil +} + +func isOwnedBy(obj ctrlClient.Object, owner *apiv2.WeightsAndBiases) bool { + for _, ref := range obj.GetOwnerReferences() { + if ref.UID == owner.UID { + return true + } + } + return false +} diff --git a/internal/controller/reconciler/reconcile_v2_infra_sizing_test.go b/internal/controller/reconciler/reconcile_v2_infra_sizing_test.go new file mode 100644 index 00000000..7ba1cbf7 --- /dev/null +++ b/internal/controller/reconciler/reconcile_v2_infra_sizing_test.go @@ -0,0 +1,258 @@ +package reconciler_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + v2 "github.com/wandb/operator/internal/controller/reconciler" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +var _ = Describe("Infra Sizing", func() { + Context("ResolveInfraSizing", func() { + It("should apply default sizing baseline", func() { + infraConfigs := map[string]serverManifest.InfraConfig{ + "default": { + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "default": { + Replicas: 1, + VolumeSize: "10Gi", + }, + }, + }, + } + defaultInfraConfig := infraConfigs["default"] + result := v2.ResolveInfraSizing(defaultInfraConfig.Sizing, "small", false) + Expect(result.Replicas).To(Equal(int32(1))) + Expect(result.VolumeSize).To(Equal("10Gi")) + }) + + It("should override default with size-specific values", func() { + infraConfigs := map[string]serverManifest.InfraConfig{ + "default": { + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "default": { + Replicas: 1, + VolumeSize: "10Gi", + }, + "small": { + Replicas: 3, + VolumeSize: "100Gi", + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + }, + }, + }, + }, + }, + } + defaultInfraConfig := infraConfigs["default"] + result := v2.ResolveInfraSizing(defaultInfraConfig.Sizing, "small", false) + Expect(result.Replicas).To(Equal(int32(3))) + Expect(result.VolumeSize).To(Equal("100Gi")) + Expect(result.Resources).NotTo(BeNil()) + Expect(result.Resources.Requests.Cpu().String()).To(Equal("2")) + }) + + It("should merge resources from default and size-specific sizing", func() { + infraConfigs := map[string]serverManifest.InfraConfig{ + "default": { + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "default": { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + }, + }, + }, + "small": { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("4"), + }, + }, + }, + }, + }, + } + defaultInfraConfig := infraConfigs["default"] + result := v2.ResolveInfraSizing(defaultInfraConfig.Sizing, "small", true) + Expect(result).NotTo(BeNil()) + // CPU request overridden by size-specific + Expect(result.Resources.Requests.Cpu().String()).To(Equal("4")) + // Memory request preserved from default + Expect(result.Resources.Requests.Memory().String()).To(Equal("2Gi")) + // CPU limit preserved from default + Expect(result.Resources.Limits.Cpu().String()).To(Equal("2")) + }) + }) + + Context("ApplyInfraSizing", func() { + It("should apply manifest sizing to empty spec fields", func() { + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + MySQL: map[string]apiv2.MySQLSpec{apiv2.DefaultInstanceName: {ManagedMysql: &apiv2.ManagedMysqlSpec{}}}, + }, + } + manifest := serverManifest.Manifest{ + Mysql: map[string]serverManifest.InfraConfig{ + "default": { + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "small": { + Replicas: 3, + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("8Gi"), + }, + }, + }, + }, + }, + }, + } + v2.ApplyInfraSizing(wandb, manifest) + Expect(wandb.Spec.MySQL[apiv2.DefaultInstanceName].ManagedMysql.Replicas).To(Equal(int32(3))) + Expect(wandb.Spec.MySQL[apiv2.DefaultInstanceName].ManagedMysql.Config.Resources.Requests.Cpu().String()).To(Equal("2")) + }) + + It("should not override user-specified spec fields", func() { + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + MySQL: map[string]apiv2.MySQLSpec{ + apiv2.DefaultInstanceName: { + ManagedMysql: &apiv2.ManagedMysqlSpec{ + Replicas: 5, + StorageSize: "50Gi", + }, + }, + }, + }, + } + manifest := serverManifest.Manifest{ + Mysql: map[string]serverManifest.InfraConfig{ + "default": { + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "small": { + Replicas: 3, + VolumeSize: "100Gi", + }, + }, + }, + }, + } + v2.ApplyInfraSizing(wandb, manifest) + Expect(wandb.Spec.MySQL[apiv2.DefaultInstanceName].ManagedMysql.Replicas).To(Equal(int32(5))) + Expect(wandb.Spec.MySQL[apiv2.DefaultInstanceName].ManagedMysql.StorageSize).To(Equal("50Gi")) + }) + + It("should default object store copies from the manifest, treating CR values as overrides", func() { + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + ObjectStore: map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: {ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{}}, + }, + }, + } + manifest := serverManifest.Manifest{ + Bucket: map[string]serverManifest.InfraConfig{ + "default": { + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "default": {Replicas: 1}, + "small": {Replicas: 3, Copies: 2}, + }, + }, + }, + } + v2.ApplyInfraSizing(wandb, manifest) + Expect(wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.Copies).To(Equal(int32(2))) + }) + + It("should not override a CR-specified object store copies value", func() { + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + ObjectStore: map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: {ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{Copies: 1}}, + }, + }, + } + manifest := serverManifest.Manifest{ + Bucket: map[string]serverManifest.InfraConfig{ + "default": { + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "small": {Replicas: 3, Copies: 2}, + }, + }, + }, + } + v2.ApplyInfraSizing(wandb, manifest) + Expect(wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.Copies).To(Equal(int32(1))) + }) + + It("should apply keeper sizing from the clickhouseKeeper block, treating CR values as overrides", func() { + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + ClickHouse: map[string]apiv2.ClickHouseSpec{ + apiv2.DefaultInstanceName: { + ManagedClickHouse: &apiv2.ManagedClickHouseSpec{ + // User explicitly set keeper storage; the manifest must not override it. + Keeper: apiv2.ClickHouseKeeperSpec{StorageSize: "20Gi"}, + }, + }, + }, + }, + } + manifest := serverManifest.Manifest{ + ClickhouseKeeper: map[string]serverManifest.InfraConfig{ + "default": { + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "default": {Replicas: 1, VolumeSize: "10Gi"}, + "small": {Replicas: 3}, + }, + }, + }, + } + v2.ApplyInfraSizing(wandb, manifest) + keeper := wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.Keeper + Expect(keeper.Replicas).To(Equal(int32(3))) // from manifest small tier + Expect(keeper.StorageSize).To(Equal("20Gi")) // user override preserved + }) + }) + + Context("ResolveKafkaSizing", func() { + It("should merge default and size-specific kafka sizing", func() { + kafkaConfig := serverManifest.KafkaConfig{ + Sizing: map[apiv2.Size]serverManifest.KafkaSizingConfig{ + "default": { + SizingConfig: serverManifest.SizingConfig{ + Replicas: 1, + VolumeSize: "10Gi", + }, + }, + "small": { + SizingConfig: serverManifest.SizingConfig{ + Replicas: 3, + VolumeSize: "100Gi", + }, + }, + }, + } + result := v2.ResolveKafkaSizing(kafkaConfig.Sizing, "small", false) + Expect(result).NotTo(BeNil()) + Expect(result.Replicas).To(Equal(int32(3))) + Expect(result.VolumeSize).To(Equal("100Gi")) + }) + }) +}) diff --git a/internal/controller/reconciler/reconcile_v2_objectstore_sizing_test.go b/internal/controller/reconciler/reconcile_v2_objectstore_sizing_test.go new file mode 100644 index 00000000..db6d87b2 --- /dev/null +++ b/internal/controller/reconciler/reconcile_v2_objectstore_sizing_test.go @@ -0,0 +1,112 @@ +package reconciler_test + +import ( + "context" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/infra/managed/objectstore/seaweedfs" + v2 "github.com/wandb/operator/internal/controller/reconciler" + seaweedv1 "github.com/wandb/operator/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// localManifestVersion is the checked-in server manifest used for local dev; the +// sizing assertions below mirror its bucket.default.sizing block. +const localManifestVersion = "0.83.0-clickhouse-keeper.2" + +func objectStoreScheme() *runtime.Scheme { + scheme := runtime.NewScheme() + Expect(apiv2.AddToScheme(scheme)).To(Succeed()) + Expect(seaweedv1.AddToScheme(scheme)).To(Succeed()) + return scheme +} + +func objectStoreWandb(size apiv2.Size) *apiv2.WeightsAndBiases { + tolerations := []corev1.Toleration{} + return &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{APIVersion: apiv2.GroupVersion.String(), Kind: "WeightsAndBiases"}, + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "wandb"}, + Spec: apiv2.WeightsAndBiasesSpec{ + Size: size, + Tolerations: &tolerations, + ObjectStore: map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: { + ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{ + Name: "object-store", + Namespace: "wandb", + Config: apiv2.ObjectStoreConfig{AccessKey: "admin"}, + }, + }, + }, + }, + } +} + +var _ = Describe("ObjectStore sizing per tier", func() { + var mfst serverManifest.Manifest + + BeforeEach(func() { + repoRoot, err := filepath.Abs(filepath.Join("..", "..", "..")) + Expect(err).NotTo(HaveOccurred()) + repository := "file://" + filepath.Join(repoRoot, "hack", "testing-manifests", "server-manifest") + mfst, err = serverManifest.GetServerManifest(context.Background(), repository, localManifestVersion) + Expect(err).NotTo(HaveOccurred()) + Expect(mfst.Bucket).To(HaveKey("default")) + }) + + // Values mirror bucket.default.sizing in the local manifest. Every tier must + // keep the same hardening regardless of size: 1024MB rollover, a writable + // volume count sized to the disk, and a small fixed filer disk. + DescribeTable("renders a healthy Seaweed spec for each size", + func(size apiv2.Size, wantReplicas int32, wantVolumeSize, wantReplication string, wantCPU string, wantFilerSize string) { + wandb := objectStoreWandb(size) + v2.ApplyInfraSizing(wandb, mfst) + + seaweed, err := seaweedfs.ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, objectStoreScheme(), mfst) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed).NotTo(BeNil()) + + Expect(seaweed.Spec.Volume.Replicas).To(Equal(wantReplicas)) + Expect(seaweed.Spec.Volume.Requests[corev1.ResourceStorage]).To(Equal(resource.MustParse(wantVolumeSize))) + Expect(seaweed.Spec.Master.DefaultReplication).NotTo(BeNil()) + Expect(*seaweed.Spec.Master.DefaultReplication).To(Equal(wantReplication)) + + // Hardening that must hold for every tier. + Expect(*seaweed.Spec.Master.VolumeSizeLimitMB).To(Equal(int32(1024))) + Expect(seaweed.Spec.Volume.MaxVolumeCounts).NotTo(BeNil()) + Expect(*seaweed.Spec.Volume.MaxVolumeCounts).To(BeNumerically(">", int32(0))) + // Filer disk follows the manifest's metadataVolumeSize per tier, falling back to 20Gi. + Expect(seaweed.Spec.Filer.Persistence.Resources.Requests[corev1.ResourceStorage]).To(Equal(resource.MustParse(wantFilerSize))) + + if wantCPU == "" { + Expect(seaweed.Spec.Volume.Requests).NotTo(HaveKey(corev1.ResourceCPU)) + } else { + Expect(seaweed.Spec.Volume.Requests[corev1.ResourceCPU]).To(Equal(resource.MustParse(wantCPU))) + } + }, + Entry("dev", apiv2.Size("dev"), int32(1), "10Gi", "000", "", "20Gi"), + Entry("micro", apiv2.Size("micro"), int32(3), "50Gi", "001", "1", "20Gi"), + Entry("small", apiv2.Size("small"), int32(3), "100Gi", "001", "2", "20Gi"), + Entry("medium", apiv2.Size("medium"), int32(3), "100Gi", "001", "4", "20Gi"), + Entry("large", apiv2.Size("large"), int32(3), "200Gi", "001", "8", "40Gi"), + Entry("xlarge", apiv2.Size("xlarge"), int32(3), "200Gi", "001", "8", "20Gi"), + Entry("2xlarge", apiv2.Size("2xlarge"), int32(3), "200Gi", "001", "8", "20Gi"), + ) + + It("lets a CR filer size override the manifest metadataVolumeSize", func() { + wandb := objectStoreWandb(apiv2.Size("large")) + wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.SeaweedObjectStoreSpec.FilerStorageSize = "100Gi" + v2.ApplyInfraSizing(wandb, mfst) + + seaweed, err := seaweedfs.ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, objectStoreScheme(), mfst) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed.Spec.Filer.Persistence.Resources.Requests[corev1.ResourceStorage]).To(Equal(resource.MustParse("100Gi"))) + }) +}) diff --git a/internal/controller/reconciler/reconcile_v2_sizing_test.go b/internal/controller/reconciler/reconcile_v2_sizing_test.go new file mode 100644 index 00000000..f1a766fe --- /dev/null +++ b/internal/controller/reconciler/reconcile_v2_sizing_test.go @@ -0,0 +1,297 @@ +package reconciler_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + v2 "github.com/wandb/operator/internal/controller/reconciler" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/utils/ptr" +) + +var _ = Describe("ReconcileV2 Sizing", func() { + Context("resolveResources", func() { + It("should apply resources from the 'default' key if present", func() { + app := serverManifest.Application{ + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "default": { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + }, + }, + }, + }, + } + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + RequireLimits: true, + }, + } + + res := v2.ResolveResources(app, wandb, nil) + Expect(res).NotTo(BeNil()) + Expect(res.Requests.Cpu().String()).To(Equal("100m")) + }) + + It("should override default with size-specific config", func() { + app := serverManifest.Application{ + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "default": { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + }, + }, + }, + "small": { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + }, + }, + }, + }, + } + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + RequireLimits: true, + }, + } + + res := v2.ResolveResources(app, wandb, nil) + Expect(res).NotTo(BeNil()) + Expect(res.Requests.Cpu().String()).To(Equal("200m")) + }) + + It("should apply container-specific overrides last", func() { + app := serverManifest.Application{ + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "small": { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + }, + }, + }, + }, + } + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + RequireLimits: true, + }, + } + containerRes := &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("300m"), + }, + } + + res := v2.ResolveResources(app, wandb, containerRes) + Expect(res).NotTo(BeNil()) + Expect(res.Requests.Cpu().String()).To(Equal("300m")) + }) + + It("should clear limits if RequireLimits is false", func() { + app := serverManifest.Application{ + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "default": { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + }, + }, + }, + }, + } + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + RequireLimits: false, + }, + } + + res := v2.ResolveResources(app, wandb, nil) + Expect(res).NotTo(BeNil()) + Expect(res.Requests.Cpu().String()).To(Equal("100m")) + Expect(res.Limits).To(BeNil()) + }) + + It("should apply legacy overrides over sizing-derived resources", func() { + app := serverManifest.Application{ + Name: "api", + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "default": { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + }, + }, + }, + }, + } + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + RequireLimits: true, + Wandb: apiv2.WandbAppSpec{ + LegacyOverrides: map[string]apiv2.LegacyOverrides{ + "api": { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("4"), + }, + }, + }, + }, + }, + }, + } + + res := v2.ResolveResources(app, wandb, nil) + Expect(res).NotTo(BeNil()) + // Override wins per resource name; untouched fields survive. + Expect(res.Requests.Cpu().String()).To(Equal("2")) + Expect(res.Requests.Memory().String()).To(Equal("128Mi")) + Expect(res.Limits.Cpu().String()).To(Equal("4")) + }) + + It("should strip legacy override limits when RequireLimits is false", func() { + app := serverManifest.Application{Name: "api"} + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + RequireLimits: false, + Wandb: apiv2.WandbAppSpec{ + LegacyOverrides: map[string]apiv2.LegacyOverrides{ + "api": { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("4"), + }, + }, + }, + }, + }, + }, + } + + res := v2.ResolveResources(app, wandb, nil) + Expect(res).NotTo(BeNil()) + Expect(res.Requests.Cpu().String()).To(Equal("2")) + Expect(res.Limits).To(BeNil()) + }) + + It("should not apply another application's legacy override", func() { + app := serverManifest.Application{ + Name: "weave", + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "default": { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + }, + }, + }, + }, + } + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + RequireLimits: true, + Wandb: apiv2.WandbAppSpec{ + LegacyOverrides: map[string]apiv2.LegacyOverrides{ + "api": { + Resources: &corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + }, + }, + }, + }, + }, + }, + } + + res := v2.ResolveResources(app, wandb, nil) + Expect(res).NotTo(BeNil()) + Expect(res.Requests.Cpu().String()).To(Equal("100m")) + }) + }) + + Context("ResolveAutoscaling", func() { + It("should use default autoscaling if size-specific is missing", func() { + app := serverManifest.Application{ + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "default": { + Autoscaling: &serverManifest.AutoscalingConfig{ + Horizontal: autoscalingv2.HorizontalPodAutoscalerSpec{ + MaxReplicas: 10, + }, + }, + }, + }, + } + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + }, + } + + hpa := v2.ResolveAutoscaling(app, wandb) + Expect(hpa).NotTo(BeNil()) + Expect(hpa.MaxReplicas).To(Equal(int32(10))) + }) + + It("should merge size-specific autoscaling onto default", func() { + app := serverManifest.Application{ + Sizing: map[apiv2.Size]serverManifest.SizingConfig{ + "default": { + Autoscaling: &serverManifest.AutoscalingConfig{ + Horizontal: autoscalingv2.HorizontalPodAutoscalerSpec{ + MinReplicas: ptr.To(int32(2)), + MaxReplicas: 10, + }, + }, + }, + "small": { + Autoscaling: &serverManifest.AutoscalingConfig{ + Horizontal: autoscalingv2.HorizontalPodAutoscalerSpec{ + MaxReplicas: 5, + }, + }, + }, + }, + } + wandb := &apiv2.WeightsAndBiases{ + Spec: apiv2.WeightsAndBiasesSpec{ + Size: "small", + }, + } + + hpa := v2.ResolveAutoscaling(app, wandb) + Expect(hpa).NotTo(BeNil()) + Expect(*hpa.MinReplicas).To(Equal(int32(2))) + Expect(hpa.MaxReplicas).To(Equal(int32(5))) + }) + }) +}) diff --git a/internal/controller/reconciler/redis.go b/internal/controller/reconciler/redis.go new file mode 100644 index 00000000..781498cc --- /dev/null +++ b/internal/controller/reconciler/redis.go @@ -0,0 +1,264 @@ +package reconciler + +import ( + "context" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/external" + externalredis "github.com/wandb/operator/internal/controller/infra/external/redis" + "github.com/wandb/operator/internal/controller/infra/managed/redis/opstree" + "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/pkg/wandb/manifest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func redisWriteState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + mfst manifest.Manifest, +) map[string][]metav1.Condition { + out := map[string][]metav1.Condition{} + for key, spec := range wandb.Spec.Redis { + switch { + case spec.ManagedRedis != nil: + out[key] = managedRedisWriteState(ctx, client, wandb, spec.ManagedRedis, mfst) + case spec.ExternalRedis != nil: + out[key] = externalredis.WriteState(ctx, client, wandb, key, spec.ExternalRedis) + } + } + return out +} + +func redisReadState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + conditions map[string][]metav1.Condition, +) (map[string][]metav1.Condition, map[string]*apiv2.RedisConnection) { + outConds := map[string][]metav1.Condition{} + outConns := map[string]*apiv2.RedisConnection{} + for key, spec := range wandb.Spec.Redis { + switch { + case spec.ManagedRedis != nil: + outConds[key], outConns[key] = managedRedisReadState(ctx, client, wandb, spec.ManagedRedis, conditions[key]) + case spec.ExternalRedis != nil: + outConds[key], outConns[key] = externalredis.ReadState(ctx, client, wandb, key, conditions[key]) + default: + outConds[key] = conditions[key] + } + } + return outConds, outConns +} + +func redisInferStatus( + ctx context.Context, + client client.Client, + recorder record.EventRecorder, + wandb *apiv2.WeightsAndBiases, + conditions map[string][]metav1.Condition, + infraConns map[string]*apiv2.RedisConnection, +) (ctrl.Result, error) { + if wandb.Status.RedisStatus == nil { + wandb.Status.RedisStatus = map[string]apiv2.RedisInfraStatus{} + } + var results []ctrl.Result + var firstErr error + for key, spec := range wandb.Spec.Redis { + var res ctrl.Result + var err error + switch { + case spec.ManagedRedis != nil: + res, err = managedRedisInferStatus(ctx, client, recorder, wandb, key, conditions[key], infraConns[key]) + case spec.ExternalRedis != nil: + res, err = externalRedisInferStatus(ctx, client, wandb, key, conditions[key], infraConns[key]) + } + results = append(results, res) + if err != nil && firstErr == nil { + firstErr = err + } + } + return consolidateResults(results), firstErr +} + +func runRedisRetentionFinalizer(ctx context.Context, c client.Client, wandb *apiv2.WeightsAndBiases, key string, spec apiv2.RedisSpec) error { + switch wandb.GetRetentionPolicy(redisInstanceInfraSpec(spec)).OnDelete { + case apiv2.PurgeOnDelete: + return redisPurgeFinalizer(ctx, c, wandb, key, spec) + case apiv2.DetachOnDelete: + return redisDetachFinalizer(ctx, c, wandb, key, spec) + } + return nil +} + +func redisInstanceInfraSpec(spec apiv2.RedisSpec) apiv2.ManagedInfraSpec { + if spec.ManagedRedis != nil { + return spec.ManagedRedis.ManagedInfraSpec + } + return apiv2.ManagedInfraSpec{} +} + +func redisPurgeFinalizer( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + key string, + spec apiv2.RedisSpec, +) error { + if managed := spec.ManagedRedis; managed != nil { + specNamespacedName := managedRedisSpecNamespacedName(managed) + onDeleteRule := opstree.ToRedisOnDeleteRule(wandb, wandb.GetRetentionPolicy(managed.ManagedInfraSpec)) + return opstree.PurgeFinalizer(ctx, client, specNamespacedName, onDeleteRule) + } + if spec.ExternalRedis != nil { + return externalredis.DeleteConnectionSecret(ctx, client, wandb, key) + } + return nil +} + +func redisDetachFinalizer( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + _ string, + spec apiv2.RedisSpec, +) error { + managed := spec.ManagedRedis + if managed == nil { + return nil + } + specNamespacedName := managedRedisSpecNamespacedName(managed) + return opstree.DetachFinalizer(ctx, client, specNamespacedName, wandb) +} + +// managed + +func managedRedisWriteState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedRedisSpec, + mfst manifest.Manifest, +) []metav1.Condition { + log := ctrl.LoggerFrom(ctx) + var specNamespacedName = managedRedisSpecNamespacedName(spec) + + standaloneDesired, err := opstree.ToRedisStandaloneVendorSpec(ctx, wandb, spec, client.Scheme(), mfst) + if err != nil { + log.Error(err, "failed to translate redis standalone spec") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ControllerErrorReason, + }, + } + } + + sentinelDesired, err := opstree.ToRedisSentinelVendorSpec(ctx, wandb, spec, client.Scheme(), mfst) + if err != nil { + log.Error(err, "failed to translate redis sentinel spec") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ControllerErrorReason, + }, + } + } + + replicationDesired, err := opstree.ToRedisReplicationVendorSpec(ctx, wandb, spec, client.Scheme(), mfst) + if err != nil { + log.Error(err, "failed to translate redis replication spec") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ControllerErrorReason, + }, + } + } + + if conditions := opstree.CheckDetached(ctx, client, specNamespacedName, wandb.GetUID()); conditions != nil { + return conditions + } + + results := opstree.WriteState(ctx, client, specNamespacedName, standaloneDesired, sentinelDesired, replicationDesired, opstree.BuildWandbRedisLabels(wandb)) + return results +} + +func managedRedisReadState( + ctx context.Context, + client client.Client, + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedRedisSpec, + newConditions []metav1.Condition, +) ([]metav1.Condition, *apiv2.RedisConnection) { + specNamespacedName := managedRedisSpecNamespacedName(spec) + onDeleteRule := opstree.ToRedisOnDeleteRule(wandb, wandb.GetRetentionPolicy(spec.ManagedInfraSpec)) + readConditions, newInfraConn := opstree.ReadState(ctx, client, specNamespacedName, wandb, onDeleteRule) + newConditions = append(newConditions, readConditions...) + return newConditions, newInfraConn +} + +func managedRedisInferStatus( + ctx context.Context, + client client.Client, + recorder record.EventRecorder, + wandb *apiv2.WeightsAndBiases, + key string, + newConditions []metav1.Condition, + newInfraConn *apiv2.RedisConnection, +) (ctrl.Result, error) { + statusBefore := wandb.DeepCopy().Status + enabled := true + oldStatus := wandb.Status.RedisStatus[key] + oldConditions := oldStatus.Conditions + oldInfraConn := oldStatus.Connection + + updatedStatus, events, ctrlResult := opstree.ComputeStatus( + ctx, + enabled, + oldConditions, + newConditions, + utils.Coalesce(newInfraConn, &oldInfraConn), + wandb.Generation, + ) + for _, e := range events { + recorder.Event(wandb, e.Type, e.Reason, e.Message) + } + wandb.Status.RedisStatus[key] = updatedStatus + err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore) + + return ctrlResult, err +} + +// external + +func externalRedisInferStatus(ctx context.Context, c client.Client, wandb *apiv2.WeightsAndBiases, key string, newConditions []metav1.Condition, newInfraConn *apiv2.RedisConnection) (ctrl.Result, error) { + statusBefore := wandb.DeepCopy().Status + oldStatus := wandb.Status.RedisStatus[key] + oldInfraConn := oldStatus.Connection + state, ready, updatedConditions := external.InferExternalStatus(oldStatus.Conditions, newConditions, wandb.Generation, newInfraConn != nil) + conn := utils.Coalesce(newInfraConn, &oldInfraConn) + + wandb.Status.RedisStatus[key] = apiv2.RedisInfraStatus{ + WBInfraStatus: apiv2.WBInfraStatus{Ready: ready, State: state, Conditions: updatedConditions}, + Connection: *conn, + } + return ctrl.Result{}, updateWandbStatusIfChanged(ctx, c, wandb, statusBefore) +} + +// helpers + +func managedRedisSpecNamespacedName(spec *apiv2.ManagedRedisSpec) types.NamespacedName { + return types.NamespacedName{ + Namespace: spec.Namespace, + Name: spec.Name, + } +} diff --git a/internal/controller/reconciler/sizing.go b/internal/controller/reconciler/sizing.go new file mode 100644 index 00000000..b1039305 --- /dev/null +++ b/internal/controller/reconciler/sizing.go @@ -0,0 +1,368 @@ +package reconciler + +import ( + "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/pkg/wandb/manifest" + v3 "k8s.io/api/autoscaling/v2" + "k8s.io/api/core/v1" +) + +func ResolveResources(app manifest.Application, wandb *v2.WeightsAndBiases, containerResources *v1.ResourceRequirements) *v1.ResourceRequirements { + var resources *v1.ResourceRequirements + + // check if there is "default" in the sizing map and apply those values + if defaultConfig, ok := app.Sizing["default"]; ok && defaultConfig.Resources != nil { + resources = mergeResources(resources, defaultConfig.Resources, wandb.Spec.RequireLimits) + } + + // check if there is a sizing config in the map that corresponds to the size in the wandb spec and apply that + if sizeConfig, ok := app.Sizing[wandb.Spec.Size]; ok && sizeConfig.Resources != nil { + resources = mergeResources(resources, sizeConfig.Resources, wandb.Spec.RequireLimits) + } + + // check if the container has a resource and if so apply those settings + resources = mergeResources(resources, containerResources, wandb.Spec.RequireLimits) + + // Legacy override wins over sizing/container resources; limits stay gated by requireLimits. + if lo, ok := wandb.Spec.Wandb.LegacyOverrides[app.Name]; ok && lo.Resources != nil { + resources = mergeResources(resources, lo.Resources, wandb.Spec.RequireLimits) + } + + if resources == nil { + return nil + } + + if len(resources.Limits) == 0 && len(resources.Requests) == 0 { + return nil + } + + return resources +} + +func ResolveAutoscaling(app manifest.Application, wandb *v2.WeightsAndBiases) *v3.HorizontalPodAutoscalerSpec { + hpa := &v3.HorizontalPodAutoscalerSpec{} + + // check if there is "default" in the sizing map and apply those values + if defaultConfig, ok := app.Sizing["default"]; ok && defaultConfig.Autoscaling != nil { + hpa.MinReplicas = defaultConfig.Autoscaling.Horizontal.MinReplicas + hpa.MaxReplicas = defaultConfig.Autoscaling.Horizontal.MaxReplicas + hpa.Metrics = defaultConfig.Autoscaling.Horizontal.Metrics + hpa.Behavior = defaultConfig.Autoscaling.Horizontal.Behavior + hpa.ScaleTargetRef = defaultConfig.Autoscaling.Horizontal.ScaleTargetRef + } + + // check if there is a sizing config in the map that corresponds to the size in the wandb spec and apply that + if sizeConfig, ok := app.Sizing[wandb.Spec.Size]; ok && sizeConfig.Autoscaling != nil { + if sizeConfig.Autoscaling.Horizontal.MinReplicas != nil { + hpa.MinReplicas = sizeConfig.Autoscaling.Horizontal.MinReplicas + } + if sizeConfig.Autoscaling.Horizontal.MaxReplicas != 0 { + hpa.MaxReplicas = sizeConfig.Autoscaling.Horizontal.MaxReplicas + } + if len(sizeConfig.Autoscaling.Horizontal.Metrics) > 0 { + hpa.Metrics = sizeConfig.Autoscaling.Horizontal.Metrics + } + if sizeConfig.Autoscaling.Horizontal.Behavior != nil { + hpa.Behavior = sizeConfig.Autoscaling.Horizontal.Behavior + } + if sizeConfig.Autoscaling.Horizontal.ScaleTargetRef.Name != "" { + hpa.ScaleTargetRef = sizeConfig.Autoscaling.Horizontal.ScaleTargetRef + } + } + + if hpa.MaxReplicas == 0 { + return nil + } + + return hpa +} + +// mergeResources merges an overlay ResourceRequirements into a base, with +// overlay values taking precedence on a per-resource-name basis. +func mergeResources(base, overlay *v1.ResourceRequirements, requireLimits bool) *v1.ResourceRequirements { + if base == nil && overlay == nil { + return nil + } + result := &v1.ResourceRequirements{} + if base != nil { + if base.Limits != nil { + result.Limits = make(v1.ResourceList) + for k, v := range base.Limits { + result.Limits[k] = v + } + } + if base.Requests != nil { + result.Requests = make(v1.ResourceList) + for k, v := range base.Requests { + result.Requests[k] = v + } + } + } + if overlay != nil { + if overlay.Limits != nil { + if result.Limits == nil { + result.Limits = make(v1.ResourceList) + } + for k, v := range overlay.Limits { + result.Limits[k] = v + } + } + if overlay.Requests != nil { + if result.Requests == nil { + result.Requests = make(v1.ResourceList) + } + for k, v := range overlay.Requests { + result.Requests[k] = v + } + } + } + + if !requireLimits { + result.Limits = nil + } + return result +} + +// ResolveInfraSizing resolves a SizingConfig from an InfraConfig map for the +// given Size. It merges the "default" sizing with the size-specific sizing, +// where size-specific values override defaults. +func ResolveInfraSizing(sizing map[v2.Size]manifest.SizingConfig, size v2.Size, requireLimits bool) *manifest.SizingConfig { + result := &manifest.SizingConfig{} + + // Apply "default" sizing baseline + if defaultSizing, ok := sizing["default"]; ok { + result.Replicas = defaultSizing.Replicas + result.Shards = defaultSizing.Shards + result.Copies = defaultSizing.Copies + result.VolumeSize = defaultSizing.VolumeSize + result.MetadataVolumeSize = defaultSizing.MetadataVolumeSize + if defaultSizing.Resources != nil { + result.Resources = defaultSizing.Resources.DeepCopy() + } + } + + // Override with size-specific sizing, merging resources + if sizeSizing, ok := sizing[size]; ok { + if sizeSizing.Replicas != 0 { + result.Replicas = sizeSizing.Replicas + } + if sizeSizing.Shards != 0 { + result.Shards = sizeSizing.Shards + } + if sizeSizing.Copies != 0 { + result.Copies = sizeSizing.Copies + } + if sizeSizing.VolumeSize != "" { + result.VolumeSize = sizeSizing.VolumeSize + } + if sizeSizing.MetadataVolumeSize != "" { + result.MetadataVolumeSize = sizeSizing.MetadataVolumeSize + } + result.Resources = mergeResources(result.Resources, sizeSizing.Resources, requireLimits) + } + + return result +} + +// ResolveKafkaSizing resolves a SizingConfig from the KafkaConfig for the given Size. +func ResolveKafkaSizing(sizing map[v2.Size]manifest.KafkaSizingConfig, size v2.Size, requireLimits bool) *manifest.KafkaSizingConfig { + result := &manifest.KafkaSizingConfig{} + + if defaultSizing, ok := sizing["default"]; ok { + result.Replicas = defaultSizing.Replicas + result.VolumeSize = defaultSizing.VolumeSize + result.ReplicationFactor = defaultSizing.ReplicationFactor + result.MinInSyncReplicas = defaultSizing.MinInSyncReplicas + result.OffsetsTopicRF = defaultSizing.OffsetsTopicRF + result.TransactionStateRF = defaultSizing.TransactionStateRF + result.TransactionStateISR = defaultSizing.TransactionStateISR + if defaultSizing.Resources != nil { + result.Resources = defaultSizing.Resources.DeepCopy() + } + } + + if sizeSizing, ok := sizing[size]; ok { + if sizeSizing.Replicas != 0 { + result.Replicas = sizeSizing.Replicas + } + if sizeSizing.VolumeSize != "" { + result.VolumeSize = sizeSizing.VolumeSize + } + if sizeSizing.ReplicationFactor != 0 { + result.ReplicationFactor = sizeSizing.ReplicationFactor + } + if sizeSizing.MinInSyncReplicas != 0 { + result.MinInSyncReplicas = sizeSizing.MinInSyncReplicas + } + if sizeSizing.OffsetsTopicRF != 0 { + result.OffsetsTopicRF = sizeSizing.OffsetsTopicRF + } + if sizeSizing.TransactionStateRF != 0 { + result.TransactionStateRF = sizeSizing.TransactionStateRF + } + if sizeSizing.TransactionStateISR != 0 { + result.TransactionStateISR = sizeSizing.TransactionStateISR + } + result.Resources = mergeResources(result.Resources, sizeSizing.Resources, requireLimits) + } + + return result +} + +// ApplyInfraSizing applies manifest-derived sizing to the wandb spec's infra +// components. Values from the manifest are only applied when the corresponding +// spec field has not been explicitly set by the user (i.e., is zero-valued). +// infraSizingConfig returns the manifest sizing config for an instance key, +// falling back to the manifest "default" config when the key has no entry. +func infraSizingConfig[T any](m map[string]T, key string) (T, bool) { + if cfg, ok := m[key]; ok { + return cfg, true + } + cfg, ok := m[v2.DefaultInstanceName] + return cfg, ok +} + +func ApplyInfraSizing(wandb *v2.WeightsAndBiases, manifest manifest.Manifest) { + size := wandb.Spec.Size + + // MySQL: size each managed instance, preferring a manifest sizing config + // matching the instance key and falling back to the manifest "default". + for key, instance := range wandb.Spec.MySQL { + spec := instance.ManagedMysql + if spec == nil { + continue + } + mysqlConfig, ok := infraSizingConfig(manifest.Mysql, key) + if !ok { + continue + } + sizing := ResolveInfraSizing(mysqlConfig.Sizing, size, wandb.Spec.RequireLimits) + if spec.Replicas == 0 && sizing.Replicas != 0 { + spec.Replicas = sizing.Replicas + } + if spec.StorageSize == "" && sizing.VolumeSize != "" { + spec.StorageSize = sizing.VolumeSize + } + if sizing.Resources != nil && len(spec.Config.Resources.Requests) == 0 && len(spec.Config.Resources.Limits) == 0 { + spec.Config.Resources = *sizing.Resources + } + } + + // Redis + for key, instance := range wandb.Spec.Redis { + spec := instance.ManagedRedis + if spec == nil { + continue + } + redisConfig, ok := infraSizingConfig(manifest.Redis, key) + if !ok { + continue + } + sizing := ResolveInfraSizing(redisConfig.Sizing, size, wandb.Spec.RequireLimits) + if spec.StorageSize == "" && sizing.VolumeSize != "" { + spec.StorageSize = sizing.VolumeSize + } + if sizing.Resources != nil && len(spec.Config.Resources.Requests) == 0 && len(spec.Config.Resources.Limits) == 0 { + spec.Config.Resources = *sizing.Resources + } + } + + // ClickHouse + for key, instance := range wandb.Spec.ClickHouse { + spec := instance.ManagedClickHouse + if spec == nil { + continue + } + + // Keeper sizing comes from the manifest's clickhouseKeeper block + // (independent of the clickhouse block); CR values are treated as user + // overrides. + if keeperConfig, ok := infraSizingConfig(manifest.ClickhouseKeeper, key); ok { + keeperSizing := ResolveInfraSizing(keeperConfig.Sizing, size, wandb.Spec.RequireLimits) + if spec.Keeper.Replicas == 0 && keeperSizing.Replicas != 0 { + spec.Keeper.Replicas = keeperSizing.Replicas + } + if spec.Keeper.StorageSize == "" && keeperSizing.VolumeSize != "" { + spec.Keeper.StorageSize = keeperSizing.VolumeSize + } + if keeperSizing.Resources != nil && len(spec.Keeper.Config.Resources.Requests) == 0 && len(spec.Keeper.Config.Resources.Limits) == 0 { + spec.Keeper.Config.Resources = *keeperSizing.Resources + } + } + + clickhouseConfig, ok := infraSizingConfig(manifest.Clickhouse, key) + if !ok { + continue + } + sizing := ResolveInfraSizing(clickhouseConfig.Sizing, size, wandb.Spec.RequireLimits) + if spec.Replicas == 0 && sizing.Replicas != 0 { + spec.Replicas = sizing.Replicas + } + if spec.StorageSize == "" && sizing.VolumeSize != "" { + spec.StorageSize = sizing.VolumeSize + } + if sizing.Resources != nil && len(spec.Config.Resources.Requests) == 0 && len(spec.Config.Resources.Limits) == 0 { + spec.Config.Resources = *sizing.Resources + } + } + + // ObjectStore (bucket) + for key, instance := range wandb.Spec.ObjectStore { + spec := instance.ManagedObjectStore + if spec == nil { + continue + } + objectStoreConfig, ok := infraSizingConfig(manifest.Bucket, key) + if !ok { + continue + } + sizing := ResolveInfraSizing(objectStoreConfig.Sizing, size, wandb.Spec.RequireLimits) + if spec.Replicas == 0 && sizing.Replicas != 0 { + spec.Replicas = sizing.Replicas + } + if spec.Copies == 0 && sizing.Copies != 0 { + spec.Copies = sizing.Copies + } + if spec.StorageSize == "" && sizing.VolumeSize != "" { + spec.StorageSize = sizing.VolumeSize + } + // Neutral manifest value maps to the SeaweedFS-specific filer disk; CR override wins. + if spec.SeaweedObjectStoreSpec.FilerStorageSize == "" && sizing.MetadataVolumeSize != "" { + spec.SeaweedObjectStoreSpec.FilerStorageSize = sizing.MetadataVolumeSize + } + if sizing.Resources != nil && len(spec.Config.Resources.Requests) == 0 && len(spec.Config.Resources.Limits) == 0 { + spec.Config.Resources = *sizing.Resources + } + } + + // Kafka + if wandb.Spec.Kafka.ManagedKafka != nil { + if sizing := ResolveKafkaSizing(manifest.Kafka.Sizing, size, wandb.Spec.RequireLimits); sizing != nil { + spec := wandb.Spec.Kafka.ManagedKafka + if spec.Replicas == 0 && sizing.Replicas != 0 { + spec.Replicas = sizing.Replicas + } + if spec.StorageSize == "" && sizing.VolumeSize != "" { + spec.StorageSize = sizing.VolumeSize + } + if sizing.Resources != nil && len(spec.Config.Resources.Requests) == 0 && len(spec.Config.Resources.Limits) == 0 { + spec.Config.Resources = *sizing.Resources + } + if spec.Config.ReplicationConfig.DefaultReplicationFactor == 0 && sizing.ReplicationFactor != 0 { + spec.Config.ReplicationConfig.DefaultReplicationFactor = sizing.ReplicationFactor + } + if spec.Config.ReplicationConfig.MinInSyncReplicas == 0 && sizing.MinInSyncReplicas != 0 { + spec.Config.ReplicationConfig.MinInSyncReplicas = sizing.MinInSyncReplicas + } + if spec.Config.ReplicationConfig.OffsetsTopicRF == 0 && sizing.OffsetsTopicRF != 0 { + spec.Config.ReplicationConfig.OffsetsTopicRF = sizing.OffsetsTopicRF + } + if spec.Config.ReplicationConfig.TransactionStateRF == 0 && sizing.TransactionStateRF != 0 { + spec.Config.ReplicationConfig.TransactionStateRF = sizing.TransactionStateRF + } + if spec.Config.ReplicationConfig.TransactionStateISR == 0 && sizing.TransactionStateISR != 0 { + spec.Config.ReplicationConfig.TransactionStateISR = sizing.TransactionStateISR + } + } + } +} diff --git a/internal/controller/reconciler/status_update.go b/internal/controller/reconciler/status_update.go new file mode 100644 index 00000000..8e7fb6d5 --- /dev/null +++ b/internal/controller/reconciler/status_update.go @@ -0,0 +1,21 @@ +package reconciler + +import ( + "context" + + apiv2 "github.com/wandb/operator/api/v2" + apiequality "k8s.io/apimachinery/pkg/api/equality" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func updateWandbStatusIfChanged( + ctx context.Context, + c client.Client, + wandb *apiv2.WeightsAndBiases, + statusBefore apiv2.WeightsAndBiasesStatus, +) error { + if apiequality.Semantic.DeepEqual(statusBefore, wandb.Status) { + return nil + } + return c.Status().Update(ctx, wandb) +} diff --git a/internal/controller/reconciler/status_update_test.go b/internal/controller/reconciler/status_update_test.go new file mode 100644 index 00000000..62d58d56 --- /dev/null +++ b/internal/controller/reconciler/status_update_test.go @@ -0,0 +1,71 @@ +package reconciler + +import ( + "context" + "testing" + + apiv2 "github.com/wandb/operator/api/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestUpdateWandbStatusIfChangedSkipsEqualStatus(t *testing.T) { + t.Parallel() + + wandb := &apiv2.WeightsAndBiases{Status: apiv2.WeightsAndBiasesStatus{Ready: true}} + if err := updateWandbStatusIfChanged(context.Background(), nil, wandb, wandb.DeepCopy().Status); err != nil { + t.Fatalf("unchanged status returned an error: %v", err) + } +} + +func TestUpdateWandbStatusIfChangedPersistsChange(t *testing.T) { + t.Parallel() + + scheme := runtime.NewScheme() + if err := apiv2.AddToScheme(scheme); err != nil { + t.Fatalf("add API to scheme: %v", err) + } + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default"}, + } + c := fake.NewClientBuilder(). + WithScheme(scheme). + WithStatusSubresource(&apiv2.WeightsAndBiases{}). + WithObjects(wandb). + Build() + + statusBefore := wandb.DeepCopy().Status + wandb.Status.Ready = true + if err := updateWandbStatusIfChanged(context.Background(), c, wandb, statusBefore); err != nil { + t.Fatalf("update status: %v", err) + } + + actual := &apiv2.WeightsAndBiases{} + if err := c.Get(context.Background(), client.ObjectKeyFromObject(wandb), actual); err != nil { + t.Fatalf("get updated resource: %v", err) + } + if !actual.Status.Ready { + t.Fatal("status change was not persisted") + } +} + +func TestApplicationManagedFieldsEqual(t *testing.T) { + t.Parallel() + + before := &apiv2.Application{ + ObjectMeta: metav1.ObjectMeta{Name: "api", Namespace: "default"}, + Spec: apiv2.ApplicationSpec{Kind: "Deployment"}, + } + after := before.DeepCopy() + after.Status.Ready = true + if !applicationManagedFieldsEqual(before, after) { + t.Fatal("status-only changes should not rewrite the Application") + } + + after.Spec.Kind = "StatefulSet" + if applicationManagedFieldsEqual(before, after) { + t.Fatal("spec changes must update the Application") + } +} diff --git a/internal/controller/reconciler/telemetry_chart_test.go b/internal/controller/reconciler/telemetry_chart_test.go new file mode 100644 index 00000000..b6b8b78e --- /dev/null +++ b/internal/controller/reconciler/telemetry_chart_test.go @@ -0,0 +1,170 @@ +package reconciler + +import ( + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestTelemetryChartFullModeRendersCoreStack(t *testing.T) { + output := runHelmTemplate(t, + "--set", "wandb-operator.enabled=false", + "--set", "telemetry.mode=full", + "--set", "victoria-metrics-operator.enabled=true", + "--set", "grafana-operator.enabled=true", + ) + + mustContain(t, output, "kind: VMSingle") + mustContain(t, output, "kind: VMAgent") + mustContain(t, output, "kind: VLSingle") + mustContain(t, output, "kind: VTSingle") + mustContain(t, output, "name: victoria-otlp-gateway-config") + mustContain(t, output, "name: victoria-otlp-gateway") + mustContain(t, output, "statsd/dogstatsd:") + mustContain(t, output, "endpoint: 0.0.0.0:8125") + mustContain(t, output, "datadog:") + mustContain(t, output, "endpoint: 0.0.0.0:8126") + mustContain(t, output, "receivers: [otlp, statsd/dogstatsd, spanmetrics]") + mustContain(t, output, "receivers: [otlp, datadog]") + mustContain(t, output, "name: statsd") + mustContain(t, output, "port: 8125") + mustContain(t, output, "protocol: UDP") + mustContain(t, output, "name: datadog-trace") + mustContain(t, output, "port: 8126") + mustContain(t, output, "kind: Grafana") + mustContain(t, output, "kind: GrafanaDatasource") + mustContain(t, output, "url: \"http://vmsingle-victoria-instance:8428\"") + mustContain(t, output, "url: \"http://vlsingle-victoria-logs:9428\"") + mustContain(t, output, "url: \"http://vtsingle-victoria-traces:10428/select/jaeger\"") + mustContain(t, output, "inputName: DS_VICTORIATRACES") + mustContain(t, output, "datasourceName: VictoriaTraces") + mustContain(t, output, "\"title\": \"Open Traces in Explore\"") + mustContain(t, output, "\"title\": \"Trace Coverage\"") + mustContain(t, output, "\"title\": \"Metadata Operation Call Rate\"") + mustContain(t, output, "\"title\": \"Redis Pool Usage by Service\"") + mustContain(t, output, "gorilla-executor") + mustContain(t, output, "\"title\": \"MySQL Exporter Up\"") + mustNotContain(t, output, "name: vmui") + mustNotContain(t, output, "name: perses") + mustContain(t, output, "retentionPeriod: \"1d\"") +} + +func TestTelemetryChartForwardModeSkipsGrafanaButAddsForwarding(t *testing.T) { + output := runHelmTemplate(t, + "--set", "wandb-operator.enabled=false", + "--set", "telemetry.mode=forward", + "--set", "telemetry.forwarding.otlp.endpoint=https://otel.example.com", + "--set", "victoria-metrics-operator.enabled=true", + ) + + mustContain(t, output, "kind: VMSingle") + mustContain(t, output, "kind: VMAgent") + mustContain(t, output, "kind: VLSingle") + mustContain(t, output, "kind: VTSingle") + mustContain(t, output, "name: victoria-otlp-gateway-config") + mustContain(t, output, "name: victoria-otlp-gateway") + mustContain(t, output, "endpoint: \"https://otel.example.com\"") + mustContain(t, output, "receivers: [otlp, statsd/dogstatsd, spanmetrics]") + mustContain(t, output, "exporters: [otlphttp/vm, otlphttp/external]") + mustContain(t, output, "exporters: [otlphttp/vl, otlphttp/external]") + mustContain(t, output, "receivers: [otlp, datadog]") + mustContain(t, output, "exporters: [otlphttp/vt, spanmetrics, otlphttp/external]") + mustNotContain(t, output, "kind: Grafana") +} + +func TestTelemetryChartOffModeSkipsManagedStack(t *testing.T) { + output := runHelmTemplate(t, + "--set", "wandb-operator.enabled=false", + "--set", "telemetry.mode=off", + ) + + mustNotContain(t, output, "kind: VMSingle") + mustNotContain(t, output, "kind: VMAgent") + mustNotContain(t, output, "kind: VLSingle") + mustNotContain(t, output, "kind: VTSingle") + mustNotContain(t, output, "name: victoria-otlp-gateway-config") + mustNotContain(t, output, "name: victoria-otlp-gateway") + mustNotContain(t, output, "kind: Grafana") + mustNotContain(t, output, "name: wandb-operator-telemetry-config") +} + +func TestTelemetryChartRendersRuntimeConfigWithoutOperatorEnvPassthrough(t *testing.T) { + output := runHelmTemplate(t, + "--set", "wandb.install=false", + "--set", "telemetry.mode=full", + "--set", "victoria-metrics-operator.enabled=true", + "--set", "grafana-operator.enabled=true", + ) + + mustContain(t, output, "name: wandb-operator-telemetry-config") + mustContain(t, output, "TELEMETRY_MODE: \"full\"") + mustContain(t, output, "TELEMETRY_OTEL_SECRET_NAME: \"wandb-otel-connection\"") + mustNotContain(t, output, "key: TELEMETRY_ENABLED") + mustNotContain(t, output, "key: TELEMETRY_MANAGED_NAMESPACE") + mustNotContain(t, output, "key: TELEMETRY_OTEL_SECRET_NAME") + mustNotContain(t, output, "key: TELEMETRY_OTEL_PROTOCOL") + mustNotContain(t, output, "key: TELEMETRY_OTEL_SERVICE_NAME") + mustNotContain(t, output, "key: TELEMETRY_OTEL_RESOURCE_ATTRIBUTES") +} + +func TestStandaloneTelemetryChartFullModeRendersCoreStack(t *testing.T) { + output := runHelmTemplateForChart(t, filepath.Join("..", "..", "..", "deploy", "telemetry"), + "--set", "mode=full", + "--set", "namespace=wandb", + ) + + mustContain(t, output, "kind: VMSingle") + mustContain(t, output, "kind: VMAgent") + mustContain(t, output, "kind: Grafana") + mustContain(t, output, "url: \"http://vmsingle-victoria-instance:8428\"") + mustContain(t, output, "statsd/dogstatsd:") + mustContain(t, output, "datadog:") +} + +func runHelmTemplate(t *testing.T, extraArgs ...string) string { + t.Helper() + output, err := runHelmTemplateWithError(t, filepath.Join("..", "..", "..", "deploy", "operator"), extraArgs...) + if err != nil { + t.Fatalf("helm template failed: %v\noutput:\n%s", err, output) + } + return output +} + +func runHelmTemplateForChart(t *testing.T, chartPath string, extraArgs ...string) string { + t.Helper() + output, err := runHelmTemplateWithError(t, chartPath, extraArgs...) + if err != nil { + t.Fatalf("helm template failed: %v\noutput:\n%s", err, output) + } + return output +} + +func runHelmTemplateWithError(t *testing.T, chartPath string, extraArgs ...string) (string, error) { + t.Helper() + if _, err := exec.LookPath("helm"); err != nil { + t.Skipf("helm binary not found: %v", err) + return "", nil + } + + args := []string{"template", "telemetry-test", chartPath, "-n", "wandb-operator", "--set", "helmHooks.enabled=false"} + args = append(args, extraArgs...) + + cmd := exec.Command("helm", args...) + output, err := cmd.CombinedOutput() + return string(output), err +} + +func mustContain(t *testing.T, output, value string) { + t.Helper() + if !strings.Contains(output, value) { + t.Fatalf("expected output to contain %q", value) + } +} + +func mustNotContain(t *testing.T, output, value string) { + t.Helper() + if strings.Contains(output, value) { + t.Fatalf("expected output not to contain %q", value) + } +} diff --git a/internal/controller/reconciler/telemetry_config.go b/internal/controller/reconciler/telemetry_config.go new file mode 100644 index 00000000..94f29ec4 --- /dev/null +++ b/internal/controller/reconciler/telemetry_config.go @@ -0,0 +1,124 @@ +package reconciler + +import ( + "fmt" + "strings" +) + +const ( + telemetryOTLPGatewayName = "victoria-otlp-gateway" + telemetryOTLPGatewayHTTPPort = 4318 + telemetryStatsdPort = 8125 + telemetryDatadogTracePort = 8126 + telemetryModeOff = "off" + telemetryModeForward = "forward" + telemetryModeFull = "full" +) + +type TelemetryEndpoints struct { + MetricsEndpoint string + LogsEndpoint string + TracesEndpoint string + StatsdAddress string + DatadogTraceAgentURL string + DatadogTraceAgentHost string + DatadogTraceAgentPort string +} + +type TelemetryOTelConfig struct { + SecretName string + Protocol string + ServiceName string + ResourceAttributes string +} + +type TelemetryRuntimeConfig struct { + Enabled bool + Mode string + Namespace string + OTel TelemetryOTelConfig +} + +func DefaultTelemetryRuntimeConfig() TelemetryRuntimeConfig { + return TelemetryRuntimeConfig{ + Enabled: false, + Mode: telemetryModeOff, + Namespace: "", + OTel: TelemetryOTelConfig{ + Protocol: "http/protobuf", + ServiceName: "wandb-service", + }, + } +} + +func (cfg *TelemetryRuntimeConfig) Normalize() { + cfg.Mode = strings.ToLower(strings.TrimSpace(cfg.Mode)) + cfg.Namespace = strings.TrimSpace(cfg.Namespace) + + cfg.OTel.SecretName = strings.TrimSpace(cfg.OTel.SecretName) + cfg.OTel.Protocol = strings.TrimSpace(cfg.OTel.Protocol) + cfg.OTel.ServiceName = strings.TrimSpace(cfg.OTel.ServiceName) + cfg.OTel.ResourceAttributes = strings.TrimSpace(cfg.OTel.ResourceAttributes) + if cfg.OTel.Protocol == "" { + cfg.OTel.Protocol = "http/protobuf" + } + if cfg.OTel.ServiceName == "" { + cfg.OTel.ServiceName = "wandb-service" + } + switch cfg.Mode { + case "": + if cfg.Enabled { + cfg.Mode = telemetryModeForward + } else { + cfg.Mode = telemetryModeOff + } + case telemetryModeOff: + if cfg.Enabled { + cfg.Mode = telemetryModeForward + } else { + cfg.Enabled = false + } + default: + cfg.Enabled = true + } +} + +func (cfg TelemetryRuntimeConfig) Validate() error { + switch cfg.Mode { + case telemetryModeOff, telemetryModeForward, telemetryModeFull: + default: + return fmt.Errorf("telemetry mode must be one of %q, %q, or %q", telemetryModeOff, telemetryModeForward, telemetryModeFull) + } + if !cfg.Enabled { + return nil + } + if strings.TrimSpace(cfg.OTel.SecretName) == "" { + return fmt.Errorf("telemetry OTel secret name must not be empty") + } + return nil +} + +func (cfg TelemetryRuntimeConfig) ResolveEndpoints() TelemetryEndpoints { + if !cfg.Enabled { + return TelemetryEndpoints{} + } + + host := resolveServiceHost(telemetryOTLPGatewayName, cfg.Namespace) + baseURL := fmt.Sprintf("http://%s:%d", host, telemetryOTLPGatewayHTTPPort) + return TelemetryEndpoints{ + MetricsEndpoint: fmt.Sprintf("%s/v1/metrics", baseURL), + LogsEndpoint: fmt.Sprintf("%s/v1/logs", baseURL), + TracesEndpoint: fmt.Sprintf("%s/v1/traces", baseURL), + StatsdAddress: fmt.Sprintf("udp://%s:%d", host, telemetryStatsdPort), + DatadogTraceAgentURL: fmt.Sprintf("http://%s:%d", host, telemetryDatadogTracePort), + DatadogTraceAgentHost: host, + DatadogTraceAgentPort: fmt.Sprintf("%d", telemetryDatadogTracePort), + } +} + +func resolveServiceHost(name, namespace string) string { + if strings.TrimSpace(namespace) == "" { + return name + } + return fmt.Sprintf("%s.%s.svc", name, namespace) +} diff --git a/internal/controller/reconciler/telemetry_configmap.go b/internal/controller/reconciler/telemetry_configmap.go new file mode 100644 index 00000000..8878e17c --- /dev/null +++ b/internal/controller/reconciler/telemetry_configmap.go @@ -0,0 +1,74 @@ +package reconciler + +import ( + "context" + "fmt" + "strconv" + "strings" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + telemetryConfigKeyEnabled = "TELEMETRY_ENABLED" + telemetryConfigKeyMode = "TELEMETRY_MODE" + telemetryConfigKeyManagedNamespace = "TELEMETRY_MANAGED_NAMESPACE" + telemetryConfigKeyOTelSecretName = "TELEMETRY_OTEL_SECRET_NAME" + telemetryConfigKeyOTelProtocol = "TELEMETRY_OTEL_PROTOCOL" + telemetryConfigKeyOTelServiceName = "TELEMETRY_OTEL_SERVICE_NAME" + telemetryConfigKeyResourceAttributes = "TELEMETRY_OTEL_RESOURCE_ATTRIBUTES" +) + +func LoadTelemetryRuntimeConfigFromConfigMap( + ctx context.Context, + client ctrlClient.Client, + ref types.NamespacedName, + defaults TelemetryRuntimeConfig, +) (TelemetryRuntimeConfig, error) { + cfg := defaults + if ref.Name == "" { + cfg.Normalize() + return cfg, cfg.Validate() + } + + cm := &corev1.ConfigMap{} + if err := client.Get(ctx, ref, cm); err != nil { + if apierrors.IsNotFound(err) { + cfg.Normalize() + return cfg, cfg.Validate() + } + return TelemetryRuntimeConfig{}, err + } + + if value, ok := cm.Data[telemetryConfigKeyEnabled]; ok && strings.TrimSpace(value) != "" { + enabled, err := strconv.ParseBool(strings.TrimSpace(value)) + if err != nil { + return TelemetryRuntimeConfig{}, fmt.Errorf("parse %s: %w", telemetryConfigKeyEnabled, err) + } + cfg.Enabled = enabled + } + if value, ok := cm.Data[telemetryConfigKeyMode]; ok { + cfg.Mode = value + } + if value, ok := cm.Data[telemetryConfigKeyManagedNamespace]; ok { + cfg.Namespace = value + } + if value, ok := cm.Data[telemetryConfigKeyOTelSecretName]; ok { + cfg.OTel.SecretName = value + } + if value, ok := cm.Data[telemetryConfigKeyOTelProtocol]; ok { + cfg.OTel.Protocol = value + } + if value, ok := cm.Data[telemetryConfigKeyOTelServiceName]; ok { + cfg.OTel.ServiceName = value + } + if value, ok := cm.Data[telemetryConfigKeyResourceAttributes]; ok { + cfg.OTel.ResourceAttributes = value + } + + cfg.Normalize() + return cfg, cfg.Validate() +} diff --git a/internal/controller/reconciler/telemetry_secret.go b/internal/controller/reconciler/telemetry_secret.go new file mode 100644 index 00000000..92d8f8ca --- /dev/null +++ b/internal/controller/reconciler/telemetry_secret.go @@ -0,0 +1,124 @@ +package reconciler + +import ( + "context" + "fmt" + "net/url" + "reflect" + "strings" + + apiv2 "github.com/wandb/operator/api/v2" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func reconcileTelemetryConnectionSecret( + ctx context.Context, + client ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, + telemetryConfig TelemetryRuntimeConfig, +) error { + if !telemetryConfig.Enabled { + return nil + } + + connection := wandb.Status.TelemetryStatus.Connection + if strings.TrimSpace(connection.ConnectionSecret) == "" { + return nil + } + + desiredData := map[string][]byte{ + "OTEL_EXPORTER_OTLP_PROTOCOL": []byte(connection.Protocol), + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": []byte(connection.MetricsEndpoint), + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": []byte(connection.LogsEndpoint), + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": []byte(connection.TracesEndpoint), + "OTEL_METRICS_EXPORTER": []byte(connection.MetricsExporter), + "OTEL_LOGS_EXPORTER": []byte(connection.LogsExporter), + "OTEL_TRACES_EXPORTER": []byte(connection.TracesExporter), + "OTEL_SERVICE_NAME": []byte(connection.ServiceName), + "OTEL_RESOURCE_ATTRIBUTES": []byte(connection.ResourceAttributes), + "GORILLA_TRACER": []byte(connection.GorillaTracer), + "GORILLA_STATSD_ADDRESS": []byte(connection.StatsdAddress), + "DD_TRACE_AGENT_URL": []byte(connection.DatadogTraceAgentURL), + "DD_AGENT_HOST": []byte(connection.DatadogTraceAgentHost), + "DD_TRACE_AGENT_PORT": []byte(connection.DatadogTraceAgentPort), + } + + secretLookup := types.NamespacedName{ + Name: connection.ConnectionSecret, + Namespace: wandb.Namespace, + } + secret := &corev1.Secret{} + err := client.Get(ctx, secretLookup, secret) + if err != nil { + if !apierrors.IsNotFound(err) { + return err + } + + secret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretLookup.Name, + Namespace: secretLookup.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + "app.kubernetes.io/component": "telemetry", + }, + }, + Type: corev1.SecretTypeOpaque, + Data: desiredData, + } + if err := controllerutil.SetOwnerReference(wandb, secret, client.Scheme()); err != nil { + return err + } + return client.Create(ctx, secret) + } + + if secret.Labels == nil { + secret.Labels = map[string]string{} + } + secret.Labels["app.kubernetes.io/managed-by"] = "wandb-operator" + secret.Labels["app.kubernetes.io/instance"] = wandb.Name + secret.Labels["app.kubernetes.io/component"] = "telemetry" + secret.Type = corev1.SecretTypeOpaque + + updated := false + if !reflect.DeepEqual(secret.Data, desiredData) { + secret.Data = desiredData + updated = true + } + + if !metav1.IsControlledBy(secret, wandb) { + if err := controllerutil.SetOwnerReference(wandb, secret, client.Scheme()); err != nil { + return err + } + updated = true + } + + if !updated { + return nil + } + return client.Update(ctx, secret) +} + +func resolveGorillaTracerConnection(protocol, tracesEndpoint string) string { + parsed, err := url.Parse(strings.TrimSpace(tracesEndpoint)) + if err != nil || parsed.Host == "" { + return "noop://" + } + + protocol = strings.ToLower(strings.TrimSpace(protocol)) + connectionType := "otlp+http" + switch { + case strings.Contains(protocol, "grpc"): + connectionType = "otlp+grpc" + case parsed.Scheme == "https": + connectionType = "otlp+https" + } + + return fmt.Sprintf("%s://%s", connectionType, parsed.Host) +} diff --git a/internal/controller/reconciler/telemetry_status.go b/internal/controller/reconciler/telemetry_status.go new file mode 100644 index 00000000..067c16ea --- /dev/null +++ b/internal/controller/reconciler/telemetry_status.go @@ -0,0 +1,255 @@ +package reconciler + +import ( + "context" + "strings" + + apiv2 "github.com/wandb/operator/api/v2" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + telemetryStateDisabled = "Disabled" + telemetryStatePending = "Pending" + telemetryStateNotReady = "NotReady" + telemetryStateReady = "Ready" + telemetryStateUnknown = "Unknown" +) + +type telemetryResourceRef struct { + name string + namespace string + gvk schema.GroupVersionKind +} + +func summarizeTelemetryInfraStatus( + ctx context.Context, + client ctrlClient.Client, + cfg TelemetryRuntimeConfig, +) apiv2.TelemetryInfraStatus { + return apiv2.TelemetryInfraStatus{ + WBInfraStatus: summarizeTelemetryWBInfraStatus(ctx, client, cfg), + Mode: cfg.Mode, + Connection: summarizeTelemetryConnectionStatus(cfg), + } +} + +func summarizeTelemetryConnectionStatus(cfg TelemetryRuntimeConfig) apiv2.TelemetryConnectionStatus { + if !cfg.Enabled { + return apiv2.TelemetryConnectionStatus{} + } + + resolvedEndpoints := cfg.ResolveEndpoints() + + return apiv2.TelemetryConnectionStatus{ + ManagedNamespace: cfg.Namespace, + ConnectionSecret: cfg.OTel.SecretName, + Protocol: cfg.OTel.Protocol, + MetricsExporter: "otlp", + LogsExporter: "otlp", + TracesExporter: "otlp", + MetricsEndpoint: resolvedEndpoints.MetricsEndpoint, + LogsEndpoint: resolvedEndpoints.LogsEndpoint, + TracesEndpoint: resolvedEndpoints.TracesEndpoint, + ServiceName: cfg.OTel.ServiceName, + ResourceAttributes: cfg.OTel.ResourceAttributes, + GorillaTracer: resolveGorillaTracerConnection(cfg.OTel.Protocol, resolvedEndpoints.TracesEndpoint), + StatsdAddress: resolvedEndpoints.StatsdAddress, + DatadogTraceAgentURL: resolvedEndpoints.DatadogTraceAgentURL, + DatadogTraceAgentHost: resolvedEndpoints.DatadogTraceAgentHost, + DatadogTraceAgentPort: resolvedEndpoints.DatadogTraceAgentPort, + } +} + +func summarizeTelemetryWBInfraStatus( + ctx context.Context, + client ctrlClient.Client, + cfg TelemetryRuntimeConfig, +) apiv2.WBInfraStatus { + if !cfg.Enabled { + return apiv2.WBInfraStatus{Ready: false, State: telemetryStateDisabled} + } + + resources := telemetryResourceRefs(cfg) + if len(resources) == 0 { + return apiv2.WBInfraStatus{Ready: false, State: telemetryStateUnknown} + } + + allReady := true + anyMissing := false + for _, resource := range resources { + ready, found, err := telemetryResourceReady(ctx, client, resource) + if err != nil { + return apiv2.WBInfraStatus{Ready: false, State: telemetryStateUnknown} + } + if !found { + anyMissing = true + allReady = false + continue + } + if !ready { + allReady = false + } + } + + switch { + case allReady: + return apiv2.WBInfraStatus{Ready: true, State: telemetryStateReady} + case anyMissing: + return apiv2.WBInfraStatus{Ready: false, State: telemetryStatePending} + default: + return apiv2.WBInfraStatus{Ready: false, State: telemetryStateNotReady} + } +} + +func telemetryResourceRefs(cfg TelemetryRuntimeConfig) []telemetryResourceRef { + namespace := cfg.Namespace + resources := []telemetryResourceRef{ + { + name: "victoria-instance", + namespace: namespace, + gvk: schema.GroupVersionKind{ + Group: "operator.victoriametrics.com", + Version: "v1beta1", + Kind: "VMSingle", + }, + }, + { + name: "victoria-agent", + namespace: namespace, + gvk: schema.GroupVersionKind{ + Group: "operator.victoriametrics.com", + Version: "v1beta1", + Kind: "VMAgent", + }, + }, + { + name: "victoria-logs", + namespace: namespace, + gvk: schema.GroupVersionKind{ + Group: "operator.victoriametrics.com", + Version: "v1", + Kind: "VLSingle", + }, + }, + { + name: "victoria-traces", + namespace: namespace, + gvk: schema.GroupVersionKind{ + Group: "operator.victoriametrics.com", + Version: "v1", + Kind: "VTSingle", + }, + }, + { + name: telemetryOTLPGatewayName, + namespace: namespace, + gvk: schema.GroupVersionKind{ + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + }, + } + + if cfg.Mode == telemetryModeFull { + resources = append(resources, telemetryResourceRef{ + name: "grafana", + namespace: namespace, + gvk: schema.GroupVersionKind{ + Group: "grafana.integreatly.org", + Version: "v1beta1", + Kind: "Grafana", + }, + }) + } + + return resources +} + +func telemetryResourceReady( + ctx context.Context, + client ctrlClient.Client, + resource telemetryResourceRef, +) (bool, bool, error) { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(resource.gvk) + + if err := client.Get(ctx, types.NamespacedName{Name: resource.name, Namespace: resource.namespace}, obj); err != nil { + if apierrors.IsNotFound(err) { + return false, false, nil + } + return false, false, err + } + + return telemetryObjectReady(obj), true, nil +} + +func telemetryObjectReady(obj *unstructured.Unstructured) bool { + if conditions := telemetryConditions(obj); len(conditions) > 0 { + if cond := findTelemetryReadyCondition(conditions); cond != nil { + return cond.Status == metav1.ConditionTrue + } + if cond := meta.FindStatusCondition(conditions, "Degraded"); cond != nil && cond.Status == metav1.ConditionTrue { + return false + } + if cond := meta.FindStatusCondition(conditions, "Progressing"); cond != nil && cond.Status == metav1.ConditionTrue { + return false + } + } + + stage, _, _ := unstructured.NestedString(obj.Object, "status", "stage") + stageStatus, _, _ := unstructured.NestedString(obj.Object, "status", "stageStatus") + updateStatus, _, _ := unstructured.NestedString(obj.Object, "status", "updateStatus") + return strings.EqualFold(stage, "complete") || + strings.EqualFold(stageStatus, "complete") || + strings.EqualFold(stageStatus, "success") || + strings.EqualFold(updateStatus, "operational") +} + +func telemetryConditions(obj *unstructured.Unstructured) []metav1.Condition { + rawConditions, found, err := unstructured.NestedSlice(obj.Object, "status", "conditions") + if err != nil || !found { + return nil + } + + conditions := make([]metav1.Condition, 0, len(rawConditions)) + for _, rawCondition := range rawConditions { + rawMap, ok := rawCondition.(map[string]any) + if !ok { + continue + } + + condition := metav1.Condition{} + if value, ok := rawMap["type"].(string); ok { + condition.Type = value + } + if value, ok := rawMap["status"].(string); ok { + condition.Status = metav1.ConditionStatus(value) + } + if value, ok := rawMap["reason"].(string); ok { + condition.Reason = value + } + if value, ok := rawMap["message"].(string); ok { + condition.Message = value + } + conditions = append(conditions, condition) + } + + return conditions +} + +func findTelemetryReadyCondition(conditions []metav1.Condition) *metav1.Condition { + for _, conditionType := range []string{"Available", "Ready", "Reconciled"} { + if cond := meta.FindStatusCondition(conditions, conditionType); cond != nil { + return cond + } + } + return nil +} diff --git a/internal/controller/reconciler/telemetry_test.go b/internal/controller/reconciler/telemetry_test.go new file mode 100644 index 00000000..f10a9968 --- /dev/null +++ b/internal/controller/reconciler/telemetry_test.go @@ -0,0 +1,1345 @@ +package reconciler + +import ( + "context" + "testing" + + apiv2 "github.com/wandb/operator/api/v2" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestTelemetryRuntimeConfigResolveEndpointsEnabled(t *testing.T) { + cfg := DefaultTelemetryRuntimeConfig() + cfg.Enabled = true + cfg.Namespace = "wandb" + cfg.Normalize() + + resolved := cfg.ResolveEndpoints() + if resolved.MetricsEndpoint != "http://victoria-otlp-gateway.wandb.svc:4318/v1/metrics" { + t.Fatalf("unexpected metrics endpoint: %s", resolved.MetricsEndpoint) + } + if resolved.LogsEndpoint != "http://victoria-otlp-gateway.wandb.svc:4318/v1/logs" { + t.Fatalf("unexpected logs endpoint: %s", resolved.LogsEndpoint) + } + if resolved.TracesEndpoint != "http://victoria-otlp-gateway.wandb.svc:4318/v1/traces" { + t.Fatalf("unexpected traces endpoint: %s", resolved.TracesEndpoint) + } + if resolved.StatsdAddress != "udp://victoria-otlp-gateway.wandb.svc:8125" { + t.Fatalf("unexpected statsd address: %s", resolved.StatsdAddress) + } + if resolved.DatadogTraceAgentURL != "http://victoria-otlp-gateway.wandb.svc:8126" { + t.Fatalf("unexpected Datadog trace agent URL: %s", resolved.DatadogTraceAgentURL) + } + if resolved.DatadogTraceAgentHost != "victoria-otlp-gateway.wandb.svc" { + t.Fatalf("unexpected Datadog trace agent host: %s", resolved.DatadogTraceAgentHost) + } + if resolved.DatadogTraceAgentPort != "8126" { + t.Fatalf("unexpected Datadog trace agent port: %s", resolved.DatadogTraceAgentPort) + } +} + +func TestTelemetryRuntimeConfigResolveEndpointsDisabled(t *testing.T) { + cfg := DefaultTelemetryRuntimeConfig() + cfg.Enabled = false + cfg.Normalize() + + resolved := cfg.ResolveEndpoints() + if resolved.MetricsEndpoint != "" || resolved.LogsEndpoint != "" || resolved.TracesEndpoint != "" { + t.Fatalf("expected empty telemetry endpoints when telemetry is disabled: %+v", resolved) + } +} + +func TestLoadTelemetryRuntimeConfigFromConfigMap(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb-operator-telemetry-config", + Namespace: "wandb-operator", + }, + Data: map[string]string{ + "TELEMETRY_ENABLED": "true", + "TELEMETRY_MODE": "full", + "TELEMETRY_MANAGED_NAMESPACE": "wandb", + "TELEMETRY_OTEL_SECRET_NAME": "custom-otel-secret", + "TELEMETRY_OTEL_PROTOCOL": "grpc", + "TELEMETRY_OTEL_SERVICE_NAME": "custom-service", + "TELEMETRY_OTEL_RESOURCE_ATTRIBUTES": "deployment.environment=dev", + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cm).Build() + + cfg, err := LoadTelemetryRuntimeConfigFromConfigMap( + context.Background(), + client, + types.NamespacedName{Name: cm.Name, Namespace: cm.Namespace}, + DefaultTelemetryRuntimeConfig(), + ) + if err != nil { + t.Fatalf("LoadTelemetryRuntimeConfigFromConfigMap returned error: %v", err) + } + + if !cfg.Enabled { + t.Fatalf("expected telemetry to be enabled") + } + if cfg.Mode != telemetryModeFull { + t.Fatalf("unexpected telemetry mode: %q", cfg.Mode) + } + if cfg.Namespace != "wandb" { + t.Fatalf("unexpected managed namespace: %q", cfg.Namespace) + } + if cfg.OTel.SecretName != "custom-otel-secret" { + t.Fatalf("unexpected secret name: %q", cfg.OTel.SecretName) + } + if cfg.OTel.Protocol != "grpc" { + t.Fatalf("unexpected protocol: %q", cfg.OTel.Protocol) + } + if cfg.OTel.ServiceName != "custom-service" { + t.Fatalf("unexpected service name: %q", cfg.OTel.ServiceName) + } + if cfg.OTel.ResourceAttributes != "deployment.environment=dev" { + t.Fatalf("unexpected resource attributes: %q", cfg.OTel.ResourceAttributes) + } +} + +func TestLoadTelemetryRuntimeConfigFromConfigMapInvalidBool(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb-operator-telemetry-config", + Namespace: "wandb-operator", + }, + Data: map[string]string{ + "TELEMETRY_ENABLED": "definitely", + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cm).Build() + + _, err := LoadTelemetryRuntimeConfigFromConfigMap( + context.Background(), + client, + types.NamespacedName{Name: cm.Name, Namespace: cm.Namespace}, + DefaultTelemetryRuntimeConfig(), + ) + if err == nil { + t.Fatalf("expected invalid telemetry enabled value to return an error") + } +} + +func TestLoadTelemetryRuntimeConfigFromConfigMapMissingSecretNameWhenEnabled(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb-operator-telemetry-config", + Namespace: "wandb-operator", + }, + Data: map[string]string{ + "TELEMETRY_MODE": "forward", + "TELEMETRY_ENABLED": "true", + "TELEMETRY_MANAGED_NAMESPACE": "wandb", + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cm).Build() + + _, err := LoadTelemetryRuntimeConfigFromConfigMap( + context.Background(), + client, + types.NamespacedName{Name: cm.Name, Namespace: cm.Namespace}, + DefaultTelemetryRuntimeConfig(), + ) + if err == nil { + t.Fatalf("expected missing telemetry secret name to return an error") + } +} + +func TestLoadTelemetryRuntimeConfigFromConfigMapMissingReturnsDisabledDefaults(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + + client := fake.NewClientBuilder().WithScheme(scheme).Build() + + defaults := DefaultTelemetryRuntimeConfig() + cfg, err := LoadTelemetryRuntimeConfigFromConfigMap( + context.Background(), + client, + types.NamespacedName{Name: "wandb-operator-telemetry-config", Namespace: "operator-system"}, + defaults, + ) + if err != nil { + t.Fatalf("LoadTelemetryRuntimeConfigFromConfigMap returned error: %v", err) + } + + if cfg != defaults { + t.Fatalf("expected missing configmap to return disabled defaults, got %#v", cfg) + } +} + +func TestSummarizeTelemetryInfraStatusForwardReady(t *testing.T) { + cfg := DefaultTelemetryRuntimeConfig() + cfg.Mode = telemetryModeForward + cfg.Enabled = true + cfg.Namespace = "wandb" + cfg.OTel.SecretName = "telemetry-secret" + cfg.Normalize() + + client := fake.NewClientBuilder().WithRuntimeObjects( + readyTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1beta1", Kind: "VMSingle"}, "victoria-instance", "wandb"), + readyTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1beta1", Kind: "VMAgent"}, "victoria-agent", "wandb"), + readyTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1", Kind: "VLSingle"}, "victoria-logs", "wandb"), + readyTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1", Kind: "VTSingle"}, "victoria-traces", "wandb"), + readyTelemetryResource(appsv1.SchemeGroupVersion.WithKind("Deployment"), telemetryOTLPGatewayName, "wandb"), + ).Build() + + status := summarizeTelemetryInfraStatus(context.Background(), client, cfg) + if !status.Ready { + t.Fatalf("expected telemetry infra status to be ready: %#v", status) + } + if status.State != telemetryStateReady { + t.Fatalf("unexpected telemetry state: %q", status.State) + } + if status.Mode != telemetryModeForward { + t.Fatalf("unexpected telemetry mode: %q", status.Mode) + } + if status.Connection.ConnectionSecret != "telemetry-secret" { + t.Fatalf("unexpected connection secret: %q", status.Connection.ConnectionSecret) + } + if status.Connection.MetricsEndpoint != "http://victoria-otlp-gateway.wandb.svc:4318/v1/metrics" { + t.Fatalf("unexpected metrics endpoint: %q", status.Connection.MetricsEndpoint) + } + if status.Connection.GorillaTracer != "otlp+http://victoria-otlp-gateway.wandb.svc:4318" { + t.Fatalf("unexpected gorilla tracer: %q", status.Connection.GorillaTracer) + } + if status.Connection.StatsdAddress != "udp://victoria-otlp-gateway.wandb.svc:8125" { + t.Fatalf("unexpected statsd address: %q", status.Connection.StatsdAddress) + } + if status.Connection.DatadogTraceAgentURL != "http://victoria-otlp-gateway.wandb.svc:8126" { + t.Fatalf("unexpected Datadog trace agent URL: %q", status.Connection.DatadogTraceAgentURL) + } + if status.Connection.DatadogTraceAgentHost != "victoria-otlp-gateway.wandb.svc" { + t.Fatalf("unexpected Datadog trace agent host: %q", status.Connection.DatadogTraceAgentHost) + } + if status.Connection.DatadogTraceAgentPort != "8126" { + t.Fatalf("unexpected Datadog trace agent port: %q", status.Connection.DatadogTraceAgentPort) + } +} + +func TestSummarizeTelemetryInfraStatusFullReady(t *testing.T) { + cfg := DefaultTelemetryRuntimeConfig() + cfg.Mode = telemetryModeFull + cfg.Enabled = true + cfg.Namespace = "wandb" + cfg.OTel.SecretName = "telemetry-secret" + cfg.Normalize() + + client := fake.NewClientBuilder().WithRuntimeObjects( + readyTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1beta1", Kind: "VMSingle"}, "victoria-instance", "wandb"), + readyTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1beta1", Kind: "VMAgent"}, "victoria-agent", "wandb"), + readyTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1", Kind: "VLSingle"}, "victoria-logs", "wandb"), + readyTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1", Kind: "VTSingle"}, "victoria-traces", "wandb"), + readyTelemetryResource(appsv1.SchemeGroupVersion.WithKind("Deployment"), telemetryOTLPGatewayName, "wandb"), + readyTelemetryResource(schema.GroupVersionKind{Group: "grafana.integreatly.org", Version: "v1beta1", Kind: "Grafana"}, "grafana", "wandb"), + ).Build() + + status := summarizeTelemetryInfraStatus(context.Background(), client, cfg) + if !status.Ready { + t.Fatalf("expected full telemetry infra status to be ready: %#v", status) + } + if status.Mode != telemetryModeFull { + t.Fatalf("unexpected telemetry mode: %q", status.Mode) + } +} + +func TestSummarizeTelemetryInfraStatusVictoriaUpdateStatusOperational(t *testing.T) { + cfg := DefaultTelemetryRuntimeConfig() + cfg.Mode = telemetryModeForward + cfg.Enabled = true + cfg.Namespace = "wandb" + cfg.OTel.SecretName = "telemetry-secret" + cfg.Normalize() + + client := fake.NewClientBuilder().WithRuntimeObjects( + operationalTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1beta1", Kind: "VMSingle"}, "victoria-instance", "wandb"), + operationalTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1beta1", Kind: "VMAgent"}, "victoria-agent", "wandb"), + operationalTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1", Kind: "VLSingle"}, "victoria-logs", "wandb"), + operationalTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1", Kind: "VTSingle"}, "victoria-traces", "wandb"), + readyTelemetryResource(appsv1.SchemeGroupVersion.WithKind("Deployment"), telemetryOTLPGatewayName, "wandb"), + ).Build() + + status := summarizeTelemetryInfraStatus(context.Background(), client, cfg) + if !status.Ready { + t.Fatalf("expected telemetry infra status to be ready for operational Victoria resources: %#v", status) + } + if status.State != telemetryStateReady { + t.Fatalf("unexpected telemetry state: %q", status.State) + } +} + +func TestSummarizeTelemetryInfraStatusDisabled(t *testing.T) { + cfg := DefaultTelemetryRuntimeConfig() + cfg.Enabled = false + cfg.Normalize() + + status := summarizeTelemetryInfraStatus(context.Background(), fake.NewClientBuilder().Build(), cfg) + if status.Ready { + t.Fatalf("expected telemetry infra status to be disabled: %#v", status) + } + if status.State != telemetryStateDisabled { + t.Fatalf("unexpected telemetry state: %q", status.State) + } + if status.Connection != (apiv2.TelemetryConnectionStatus{}) { + t.Fatalf("expected disabled telemetry connection status to be empty: %#v", status.Connection) + } +} + +func TestSummarizeTelemetryInfraStatusMissingStackIsPending(t *testing.T) { + cfg := DefaultTelemetryRuntimeConfig() + cfg.Mode = telemetryModeForward + cfg.Enabled = true + cfg.Namespace = "wandb" + cfg.OTel.SecretName = "telemetry-secret" + cfg.Normalize() + + status := summarizeTelemetryInfraStatus(context.Background(), fake.NewClientBuilder().Build(), cfg) + if status.Ready { + t.Fatalf("expected telemetry infra status to be pending when stack is missing: %#v", status) + } + if status.State != telemetryStatePending { + t.Fatalf("unexpected telemetry state: %q", status.State) + } +} + +func TestSummarizeTelemetryInfraStatusDegradedIsNotReady(t *testing.T) { + cfg := DefaultTelemetryRuntimeConfig() + cfg.Mode = telemetryModeForward + cfg.Enabled = true + cfg.Namespace = "wandb" + cfg.OTel.SecretName = "telemetry-secret" + cfg.Normalize() + + client := fake.NewClientBuilder().WithRuntimeObjects( + newTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1beta1", Kind: "VMSingle"}, "victoria-instance", "wandb", []map[string]any{{"type": "Degraded", "status": "True"}}), + readyTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1beta1", Kind: "VMAgent"}, "victoria-agent", "wandb"), + readyTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1", Kind: "VLSingle"}, "victoria-logs", "wandb"), + readyTelemetryResource(schema.GroupVersionKind{Group: "operator.victoriametrics.com", Version: "v1", Kind: "VTSingle"}, "victoria-traces", "wandb"), + readyTelemetryResource(appsv1.SchemeGroupVersion.WithKind("Deployment"), telemetryOTLPGatewayName, "wandb"), + ).Build() + + status := summarizeTelemetryInfraStatus(context.Background(), client, cfg) + if status.Ready { + t.Fatalf("expected degraded telemetry infra status to be not ready: %#v", status) + } + if status.State != telemetryStateNotReady { + t.Fatalf("unexpected telemetry state: %q", status.State) + } +} + +func TestReconcileTelemetryConnectionSecretCreateManaged(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + if err := apiv2.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding appsv2 to scheme: %v", err) + } + + wandb := &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{APIVersion: "apps.wandb.com/v2", Kind: "WeightsAndBiases"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "default", + }, + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(wandb).Build() + + cfg := DefaultTelemetryRuntimeConfig() + cfg.Enabled = true + cfg.OTel.SecretName = "wandb-otel-connection" + cfg.Normalize() + wandb.Status.TelemetryStatus = summarizeTelemetryInfraStatus(context.Background(), client, cfg) + + if err := reconcileTelemetryConnectionSecret(context.Background(), client, wandb, cfg); err != nil { + t.Fatalf("reconcileTelemetryConnectionSecret returned error: %v", err) + } + + secret := &corev1.Secret{} + lookup := types.NamespacedName{Name: cfg.OTel.SecretName, Namespace: wandb.Namespace} + if err := client.Get(context.Background(), lookup, secret); err != nil { + t.Fatalf("failed retrieving telemetry secret: %v", err) + } + + if got := string(secret.Data["OTEL_EXPORTER_OTLP_PROTOCOL"]); got != "http/protobuf" { + t.Fatalf("unexpected protocol in secret: %q", got) + } + if got := string(secret.Data["OTEL_METRICS_EXPORTER"]); got != "otlp" { + t.Fatalf("unexpected metrics exporter in secret: %q", got) + } + if got := string(secret.Data["OTEL_LOGS_EXPORTER"]); got != "otlp" { + t.Fatalf("unexpected logs exporter in secret: %q", got) + } + if got := string(secret.Data["OTEL_TRACES_EXPORTER"]); got != "otlp" { + t.Fatalf("unexpected traces exporter in secret: %q", got) + } + if got := string(secret.Data["GORILLA_TRACER"]); got != "otlp+http://victoria-otlp-gateway:4318" { + t.Fatalf("unexpected gorilla tracer connection in secret: %q", got) + } + if got := string(secret.Data["GORILLA_STATSD_ADDRESS"]); got != "udp://victoria-otlp-gateway:8125" { + t.Fatalf("unexpected gorilla statsd address in secret: %q", got) + } + if got := string(secret.Data["DD_TRACE_AGENT_URL"]); got != "http://victoria-otlp-gateway:8126" { + t.Fatalf("unexpected Datadog trace agent URL in secret: %q", got) + } + if got := string(secret.Data["DD_AGENT_HOST"]); got != "victoria-otlp-gateway" { + t.Fatalf("unexpected Datadog agent host in secret: %q", got) + } + if got := string(secret.Data["DD_TRACE_AGENT_PORT"]); got != "8126" { + t.Fatalf("unexpected Datadog trace agent port in secret: %q", got) + } + if got := string(secret.Data["OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"]); got != "http://victoria-otlp-gateway:4318/v1/metrics" { + t.Fatalf("unexpected metrics endpoint in secret: %q", got) + } + if got := string(secret.Data["OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"]); got != "http://victoria-otlp-gateway:4318/v1/logs" { + t.Fatalf("unexpected logs endpoint in secret: %q", got) + } + if got := string(secret.Data["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"]); got != "http://victoria-otlp-gateway:4318/v1/traces" { + t.Fatalf("unexpected traces endpoint in secret: %q", got) + } + if len(secret.OwnerReferences) != 1 || secret.OwnerReferences[0].Name != wandb.Name { + t.Fatalf("expected secret to be owned by wandb resource") + } +} + +func TestReconcileTelemetryConnectionSecretDisabledSkipsCreate(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + if err := apiv2.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding appsv2 to scheme: %v", err) + } + + wandb := &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{APIVersion: "apps.wandb.com/v2", Kind: "WeightsAndBiases"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "default", + }, + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(wandb).Build() + + cfg := DefaultTelemetryRuntimeConfig() + cfg.Enabled = false + cfg.OTel.SecretName = "wandb-otel-connection" + cfg.Normalize() + + if err := reconcileTelemetryConnectionSecret(context.Background(), client, wandb, cfg); err != nil { + t.Fatalf("reconcileTelemetryConnectionSecret returned error: %v", err) + } + + secret := &corev1.Secret{} + lookup := types.NamespacedName{Name: cfg.OTel.SecretName, Namespace: wandb.Namespace} + if err := client.Get(context.Background(), lookup, secret); !apierrors.IsNotFound(err) { + t.Fatalf("expected telemetry secret not to be created, got error %v", err) + } +} + +func TestReconcileTelemetryConnectionSecretUpdateManaged(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + if err := apiv2.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding appsv2 to scheme: %v", err) + } + + wandb := &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{APIVersion: "apps.wandb.com/v2", Kind: "WeightsAndBiases"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "default", + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb-otel-connection", + Namespace: "default", + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": []byte("http://old.example/metrics"), + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(wandb, secret).Build() + + cfg := DefaultTelemetryRuntimeConfig() + cfg.Enabled = true + cfg.Namespace = "wandb" + cfg.OTel.SecretName = "wandb-otel-connection" + cfg.Normalize() + wandb.Status.TelemetryStatus = summarizeTelemetryInfraStatus(context.Background(), client, cfg) + + if err := reconcileTelemetryConnectionSecret(context.Background(), client, wandb, cfg); err != nil { + t.Fatalf("reconcileTelemetryConnectionSecret returned error: %v", err) + } + + updated := &corev1.Secret{} + lookup := types.NamespacedName{Name: cfg.OTel.SecretName, Namespace: wandb.Namespace} + if err := client.Get(context.Background(), lookup, updated); err != nil { + t.Fatalf("failed retrieving updated telemetry secret: %v", err) + } + + if got := string(updated.Data["OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"]); got != "http://victoria-otlp-gateway.wandb.svc:4318/v1/metrics" { + t.Fatalf("unexpected metrics endpoint: %q", got) + } + if got := string(updated.Data["OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"]); got != "http://victoria-otlp-gateway.wandb.svc:4318/v1/logs" { + t.Fatalf("unexpected logs endpoint: %q", got) + } + if got := string(updated.Data["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"]); got != "http://victoria-otlp-gateway.wandb.svc:4318/v1/traces" { + t.Fatalf("unexpected traces endpoint: %q", got) + } + if got := string(updated.Data["GORILLA_TRACER"]); got != "otlp+http://victoria-otlp-gateway.wandb.svc:4318" { + t.Fatalf("unexpected gorilla tracer connection in updated secret: %q", got) + } + if got := string(updated.Data["GORILLA_STATSD_ADDRESS"]); got != "udp://victoria-otlp-gateway.wandb.svc:8125" { + t.Fatalf("unexpected gorilla statsd address in updated secret: %q", got) + } + if got := string(updated.Data["DD_TRACE_AGENT_URL"]); got != "http://victoria-otlp-gateway.wandb.svc:8126" { + t.Fatalf("unexpected Datadog trace agent URL in updated secret: %q", got) + } + if got := string(updated.Data["DD_AGENT_HOST"]); got != "victoria-otlp-gateway.wandb.svc" { + t.Fatalf("unexpected Datadog agent host in updated secret: %q", got) + } + if got := string(updated.Data["DD_TRACE_AGENT_PORT"]); got != "8126" { + t.Fatalf("unexpected Datadog trace agent port in updated secret: %q", got) + } +} + +func TestResolveGorillaTracerConnection(t *testing.T) { + tests := []struct { + name string + protocol string + tracesEndpoint string + want string + }{ + { + name: "http protobuf defaults to otlp+http", + protocol: "http/protobuf", + tracesEndpoint: "http://vtsingle-victoria-traces:10428/insert/opentelemetry/v1/traces", + want: "otlp+http://vtsingle-victoria-traces:10428", + }, + { + name: "https endpoint maps to otlp+https", + protocol: "http/protobuf", + tracesEndpoint: "https://traces.example.com/v1/traces", + want: "otlp+https://traces.example.com", + }, + { + name: "grpc protocol maps to otlp+grpc", + protocol: "grpc", + tracesEndpoint: "http://otel-collector.default.svc:4317", + want: "otlp+grpc://otel-collector.default.svc:4317", + }, + { + name: "invalid endpoint falls back to noop", + protocol: "http/protobuf", + tracesEndpoint: "not-a-url", + want: "noop://", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := resolveGorillaTracerConnection(tc.protocol, tc.tracesEndpoint) + if got != tc.want { + t.Fatalf("unexpected gorilla tracer connection: got %q want %q", got, tc.want) + } + }) + } +} + +//func TestResolveEnvvarsTelemetrySource(t *testing.T) { +// scheme := runtime.NewScheme() +// if err := corev1.AddToScheme(scheme); err != nil { +// t.Fatalf("failed adding corev1 to scheme: %v", err) +// } +// client := fake.NewClientBuilder().WithScheme(scheme).Build() +// +// wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default"}} +// manifest := serverManifest.Manifest{} +// envs := []serverManifest.EnvVar{ +// { +// Name: "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", +// Sources: []serverManifest.EnvSource{ +// {Type: "telemetry", Field: "metricsEndpoint"}, +// }, +// }, +// { +// Name: "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", +// Sources: []serverManifest.EnvSource{ +// {Type: "telemetry", Name: "custom-otel", Field: "logsEndpoint"}, +// }, +// }, +// { +// Name: "OTEL_TRACES_EXPORTER", +// Sources: []serverManifest.EnvSource{ +// {Type: "telemetry", Field: "tracesExporter"}, +// }, +// }, +// { +// Name: "GORILLA_TRACER", +// Sources: []serverManifest.EnvSource{ +// {Type: "telemetry", Field: "gorillaTracer"}, +// }, +// }, +// { +// Name: "OTEL_PROTOCOL_AND_SERVICE", +// Sources: []serverManifest.EnvSource{ +// {Type: "telemetry", Field: "protocol"}, +// {Type: "telemetry", Field: "serviceName"}, +// }, +// }, +// } +// +// resolved, err := resolveEnvvars(context.Background(), client, wandb, manifest, nil, envs) +// if err != nil { +// t.Fatalf("resolveEnvvars returned error: %v", err) +// } +// +// metricsEnv := mustFindEnvVar(t, resolved, "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT") +// if metricsEnv.ValueFrom == nil || metricsEnv.ValueFrom.SecretKeyRef == nil { +// t.Fatalf("expected metrics endpoint to resolve from secret key ref") +// } +// if metricsEnv.ValueFrom.SecretKeyRef.Name != "wandb-otel-connection" { +// t.Fatalf("unexpected metrics secret name: %s", metricsEnv.ValueFrom.SecretKeyRef.Name) +// } +// if metricsEnv.ValueFrom.SecretKeyRef.Key != "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" { +// t.Fatalf("unexpected metrics key: %s", metricsEnv.ValueFrom.SecretKeyRef.Key) +// } +// +// logsEnv := mustFindEnvVar(t, resolved, "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT") +// if logsEnv.ValueFrom == nil || logsEnv.ValueFrom.SecretKeyRef == nil { +// t.Fatalf("expected logs endpoint to resolve from secret key ref") +// } +// if logsEnv.ValueFrom.SecretKeyRef.Name != "custom-otel" { +// t.Fatalf("unexpected logs secret name: %s", logsEnv.ValueFrom.SecretKeyRef.Name) +// } +// if logsEnv.ValueFrom.SecretKeyRef.Key != "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT" { +// t.Fatalf("unexpected logs key: %s", logsEnv.ValueFrom.SecretKeyRef.Key) +// } +// +// tracesExporter := mustFindEnvVar(t, resolved, "OTEL_TRACES_EXPORTER") +// if tracesExporter.ValueFrom == nil || tracesExporter.ValueFrom.SecretKeyRef == nil { +// t.Fatalf("expected traces exporter to resolve from secret key ref") +// } +// if tracesExporter.ValueFrom.SecretKeyRef.Name != "wandb-otel-connection" { +// t.Fatalf("unexpected traces exporter secret name: %s", tracesExporter.ValueFrom.SecretKeyRef.Name) +// } +// if tracesExporter.ValueFrom.SecretKeyRef.Key != "OTEL_TRACES_EXPORTER" { +// t.Fatalf("unexpected traces exporter key: %s", tracesExporter.ValueFrom.SecretKeyRef.Key) +// } +// +// gorillaTracer := mustFindEnvVar(t, resolved, "GORILLA_TRACER") +// if gorillaTracer.ValueFrom == nil || gorillaTracer.ValueFrom.SecretKeyRef == nil { +// t.Fatalf("expected gorilla tracer to resolve from secret key ref") +// } +// if gorillaTracer.ValueFrom.SecretKeyRef.Name != "wandb-otel-connection" { +// t.Fatalf("unexpected gorilla tracer secret name: %s", gorillaTracer.ValueFrom.SecretKeyRef.Name) +// } +// if gorillaTracer.ValueFrom.SecretKeyRef.Key != "GORILLA_TRACER" { +// t.Fatalf("unexpected gorilla tracer key: %s", gorillaTracer.ValueFrom.SecretKeyRef.Key) +// } +// +// protocolComponent := mustFindEnvVar(t, resolved, "OTEL_PROTOCOL_AND_SERVICE_0") +// if protocolComponent.ValueFrom == nil || protocolComponent.ValueFrom.SecretKeyRef == nil { +// t.Fatalf("expected protocol component to resolve from secret") +// } +// if protocolComponent.ValueFrom.SecretKeyRef.Key != "OTEL_EXPORTER_OTLP_PROTOCOL" { +// t.Fatalf("unexpected protocol key: %s", protocolComponent.ValueFrom.SecretKeyRef.Key) +// } +// +// serviceComponent := mustFindEnvVar(t, resolved, "OTEL_PROTOCOL_AND_SERVICE_1") +// if serviceComponent.ValueFrom == nil || serviceComponent.ValueFrom.SecretKeyRef == nil { +// t.Fatalf("expected service component to resolve from secret") +// } +// if serviceComponent.ValueFrom.SecretKeyRef.Key != "OTEL_SERVICE_NAME" { +// t.Fatalf("unexpected service key: %s", serviceComponent.ValueFrom.SecretKeyRef.Key) +// } +// +// joined := mustFindEnvVar(t, resolved, "OTEL_PROTOCOL_AND_SERVICE") +// if joined.Value != "$(OTEL_PROTOCOL_AND_SERVICE_0),$(OTEL_PROTOCOL_AND_SERVICE_1)" { +// t.Fatalf("unexpected joined telemetry env value: %s", joined.Value) +// } +//} + +func TestResolveEnvvarsTelemetrySourceUsesStatusSecret(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + client := fake.NewClientBuilder().WithScheme(scheme).Build() + + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default"}, + Status: apiv2.WeightsAndBiasesStatus{ + TelemetryStatus: apiv2.TelemetryInfraStatus{ + Connection: apiv2.TelemetryConnectionStatus{ + ConnectionSecret: "status-otel-secret", + }, + }, + }, + } + envs := []serverManifest.EnvVar{ + { + Name: "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "metricsEndpoint"}, + }, + }, + { + Name: "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Name: "custom-otel", Field: "logsEndpoint"}, + }, + }, + { + Name: "OTEL_TRACES_EXPORTER", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "tracesExporter"}, + }, + }, + { + Name: "GORILLA_TRACER", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "gorillaTracer"}, + }, + }, + { + Name: "GORILLA_STATSD_ADDRESS", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "statsdAddress"}, + }, + }, + { + Name: "DD_TRACE_AGENT_URL", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "datadogTraceAgentURL"}, + }, + }, + { + Name: "DD_AGENT_HOST", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "datadogTraceAgentHost"}, + }, + }, + { + Name: "DD_TRACE_AGENT_PORT", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "datadogTraceAgentPort"}, + }, + }, + { + Name: "OTEL_PROTOCOL_AND_SERVICE", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "protocol"}, + {Type: "telemetry", Field: "serviceName"}, + }, + }, + } + + resolved, err := resolveEnvvars(context.Background(), client, wandb, serverManifest.Manifest{}, nil, envs) + if err != nil { + t.Fatalf("resolveEnvvars returned error: %v", err) + } + + metricsEnv := mustFindEnvVar(t, resolved, "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT") + if metricsEnv.ValueFrom == nil || metricsEnv.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected metrics endpoint to resolve from secret key ref") + } + if metricsEnv.ValueFrom.SecretKeyRef.Name != "status-otel-secret" { + t.Fatalf("unexpected metrics secret name: %s", metricsEnv.ValueFrom.SecretKeyRef.Name) + } + if metricsEnv.ValueFrom.SecretKeyRef.Key != "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" { + t.Fatalf("unexpected metrics key: %s", metricsEnv.ValueFrom.SecretKeyRef.Key) + } + + logsEnv := mustFindEnvVar(t, resolved, "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT") + if logsEnv.ValueFrom == nil || logsEnv.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected logs endpoint to resolve from secret key ref") + } + if logsEnv.ValueFrom.SecretKeyRef.Name != "custom-otel" { + t.Fatalf("unexpected logs secret name: %s", logsEnv.ValueFrom.SecretKeyRef.Name) + } + if logsEnv.ValueFrom.SecretKeyRef.Key != "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT" { + t.Fatalf("unexpected logs key: %s", logsEnv.ValueFrom.SecretKeyRef.Key) + } + + tracesExporter := mustFindEnvVar(t, resolved, "OTEL_TRACES_EXPORTER") + if tracesExporter.ValueFrom == nil || tracesExporter.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected traces exporter to resolve from secret key ref") + } + if tracesExporter.ValueFrom.SecretKeyRef.Name != "status-otel-secret" { + t.Fatalf("unexpected traces exporter secret name: %s", tracesExporter.ValueFrom.SecretKeyRef.Name) + } + if tracesExporter.ValueFrom.SecretKeyRef.Key != "OTEL_TRACES_EXPORTER" { + t.Fatalf("unexpected traces exporter key: %s", tracesExporter.ValueFrom.SecretKeyRef.Key) + } + + gorillaTracer := mustFindEnvVar(t, resolved, "GORILLA_TRACER") + if gorillaTracer.ValueFrom == nil || gorillaTracer.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected gorilla tracer to resolve from secret key ref") + } + if gorillaTracer.ValueFrom.SecretKeyRef.Name != "status-otel-secret" { + t.Fatalf("unexpected gorilla tracer secret name: %s", gorillaTracer.ValueFrom.SecretKeyRef.Name) + } + if gorillaTracer.ValueFrom.SecretKeyRef.Key != "GORILLA_TRACER" { + t.Fatalf("unexpected gorilla tracer key: %s", gorillaTracer.ValueFrom.SecretKeyRef.Key) + } + + statsdAddress := mustFindEnvVar(t, resolved, "GORILLA_STATSD_ADDRESS") + if statsdAddress.ValueFrom == nil || statsdAddress.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected gorilla statsd address to resolve from secret key ref") + } + if statsdAddress.ValueFrom.SecretKeyRef.Name != "status-otel-secret" { + t.Fatalf("unexpected gorilla statsd secret name: %s", statsdAddress.ValueFrom.SecretKeyRef.Name) + } + if statsdAddress.ValueFrom.SecretKeyRef.Key != "GORILLA_STATSD_ADDRESS" { + t.Fatalf("unexpected gorilla statsd key: %s", statsdAddress.ValueFrom.SecretKeyRef.Key) + } + + ddTraceAgentURL := mustFindEnvVar(t, resolved, "DD_TRACE_AGENT_URL") + if ddTraceAgentURL.ValueFrom == nil || ddTraceAgentURL.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected Datadog trace agent URL to resolve from secret key ref") + } + if ddTraceAgentURL.ValueFrom.SecretKeyRef.Name != "status-otel-secret" { + t.Fatalf("unexpected Datadog trace agent URL secret name: %s", ddTraceAgentURL.ValueFrom.SecretKeyRef.Name) + } + if ddTraceAgentURL.ValueFrom.SecretKeyRef.Key != "DD_TRACE_AGENT_URL" { + t.Fatalf("unexpected Datadog trace agent URL key: %s", ddTraceAgentURL.ValueFrom.SecretKeyRef.Key) + } + + ddAgentHost := mustFindEnvVar(t, resolved, "DD_AGENT_HOST") + if ddAgentHost.ValueFrom == nil || ddAgentHost.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected Datadog agent host to resolve from secret key ref") + } + if ddAgentHost.ValueFrom.SecretKeyRef.Name != "status-otel-secret" { + t.Fatalf("unexpected Datadog agent host secret name: %s", ddAgentHost.ValueFrom.SecretKeyRef.Name) + } + if ddAgentHost.ValueFrom.SecretKeyRef.Key != "DD_AGENT_HOST" { + t.Fatalf("unexpected Datadog agent host key: %s", ddAgentHost.ValueFrom.SecretKeyRef.Key) + } + + ddTraceAgentPort := mustFindEnvVar(t, resolved, "DD_TRACE_AGENT_PORT") + if ddTraceAgentPort.ValueFrom == nil || ddTraceAgentPort.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected Datadog trace agent port to resolve from secret key ref") + } + if ddTraceAgentPort.ValueFrom.SecretKeyRef.Name != "status-otel-secret" { + t.Fatalf("unexpected Datadog trace agent port secret name: %s", ddTraceAgentPort.ValueFrom.SecretKeyRef.Name) + } + if ddTraceAgentPort.ValueFrom.SecretKeyRef.Key != "DD_TRACE_AGENT_PORT" { + t.Fatalf("unexpected Datadog trace agent port key: %s", ddTraceAgentPort.ValueFrom.SecretKeyRef.Key) + } + + protocolComponent := mustFindEnvVar(t, resolved, "OTEL_PROTOCOL_AND_SERVICE_0") + if protocolComponent.ValueFrom == nil || protocolComponent.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected protocol component to resolve from secret") + } + if protocolComponent.ValueFrom.SecretKeyRef.Name != "status-otel-secret" { + t.Fatalf("unexpected protocol secret name: %s", protocolComponent.ValueFrom.SecretKeyRef.Name) + } + if protocolComponent.ValueFrom.SecretKeyRef.Key != "OTEL_EXPORTER_OTLP_PROTOCOL" { + t.Fatalf("unexpected protocol key: %s", protocolComponent.ValueFrom.SecretKeyRef.Key) + } + + serviceComponent := mustFindEnvVar(t, resolved, "OTEL_PROTOCOL_AND_SERVICE_1") + if serviceComponent.ValueFrom == nil || serviceComponent.ValueFrom.SecretKeyRef == nil { + t.Fatalf("expected service component to resolve from secret") + } + if serviceComponent.ValueFrom.SecretKeyRef.Name != "status-otel-secret" { + t.Fatalf("unexpected service secret name: %s", serviceComponent.ValueFrom.SecretKeyRef.Name) + } + if serviceComponent.ValueFrom.SecretKeyRef.Key != "OTEL_SERVICE_NAME" { + t.Fatalf("unexpected service key: %s", serviceComponent.ValueFrom.SecretKeyRef.Key) + } + + joined := mustFindEnvVar(t, resolved, "OTEL_PROTOCOL_AND_SERVICE") + if joined.Value != "$(OTEL_PROTOCOL_AND_SERVICE_0),$(OTEL_PROTOCOL_AND_SERVICE_1)" { + t.Fatalf("unexpected joined telemetry env value: %s", joined.Value) + } +} + +func TestResolveEnvvarsTelemetrySourceWithoutStatusSecretSkipsEnv(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + client := fake.NewClientBuilder().WithScheme(scheme).Build() + + wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default"}} + envs := []serverManifest.EnvVar{ + { + Name: "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + Sources: []serverManifest.EnvSource{ + {Type: "telemetry", Field: "metricsEndpoint"}, + }, + }, + } + + resolved, err := resolveEnvvars(context.Background(), client, wandb, serverManifest.Manifest{}, nil, envs) + if err != nil { + t.Fatalf("resolveEnvvars returned error: %v", err) + } + if len(resolved) != 0 { + t.Fatalf("expected telemetry source without status secret to be skipped, got %#v", resolved) + } +} + +func TestResolveEnvvarsServiceSourceFromManifest(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + client := fake.NewClientBuilder().WithScheme(scheme).Build() + + wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: "wandb-dev-v2", Namespace: "default"}} + manifest := serverManifest.Manifest{ + Applications: map[string]serverManifest.Application{ + "anaconda2": { + Name: "anaconda2", + Service: &serverManifest.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Name: "anaconda2", Port: 8080}, + }, + }, + }, + }, + } + envs := []serverManifest.EnvVar{ + { + Name: "GORILLA_SWEEP_PROVIDER", + Sources: []serverManifest.EnvSource{ + {Type: "service", Name: "anaconda2", Proto: "http"}, + }, + }, + } + + resolved, err := resolveEnvvars(context.Background(), client, wandb, manifest, nil, envs) + if err != nil { + t.Fatalf("resolveEnvvars returned error: %v", err) + } + + sweepProvider := mustFindEnvVar(t, resolved, "GORILLA_SWEEP_PROVIDER") + if sweepProvider.Value != "http://anaconda2:8080" { + t.Fatalf("unexpected sweep provider value: %s", sweepProvider.Value) + } +} + +func TestResolveEnvvarsServiceSourcePortNameFromManifest(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + client := fake.NewClientBuilder().WithScheme(scheme).Build() + + wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: "wandb-dev-v2", Namespace: "default"}} + manifest := serverManifest.Manifest{ + Applications: map[string]serverManifest.Application{ + "parquet": { + Name: "parquet", + Service: &serverManifest.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Name: "api", Port: 8080}, + {Name: "parquet", Port: 9000}, + }, + }, + }, + }, + } + envs := []serverManifest.EnvVar{ + { + Name: "GORILLA_HISTORY_STORE", + Sources: []serverManifest.EnvSource{ + {Type: "service", Name: "parquet", Port: "parquet", Proto: "http", Path: "/_goRPC_"}, + }, + }, + } + + resolved, err := resolveEnvvars(context.Background(), client, wandb, manifest, nil, envs) + if err != nil { + t.Fatalf("resolveEnvvars returned error: %v", err) + } + + historyStore := mustFindEnvVar(t, resolved, "GORILLA_HISTORY_STORE") + if historyStore.Value != "http://parquet:9000/_goRPC_" { + t.Fatalf("unexpected history store value: %s", historyStore.Value) + } +} + +func TestApplyWorkloadTelemetryDefaultsOverridesSharedServiceName(t *testing.T) { + envVars := []corev1.EnvVar{ + { + Name: "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "wandb-otel-connection"}, + Key: "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + }, + }, + }, + { + Name: "OTEL_SERVICE_NAME", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "wandb-otel-connection"}, + Key: "OTEL_SERVICE_NAME", + }, + }, + }, + } + + resolved := applyWorkloadTelemetryDefaults(envVars, "parquet") + + serviceName := mustFindEnvVar(t, resolved, "OTEL_SERVICE_NAME") + if serviceName.Value != "parquet" { + t.Fatalf("expected workload-specific OTEL service name, got %q", serviceName.Value) + } + if serviceName.ValueFrom != nil { + t.Fatalf("expected workload-specific OTEL service name to be a literal override") + } +} + +func TestApplyWorkloadTelemetryDefaultsPreservesExplicitServiceName(t *testing.T) { + envVars := []corev1.EnvVar{ + {Name: "OTEL_METRICS_EXPORTER", Value: "otlp"}, + {Name: "OTEL_SERVICE_NAME", Value: "custom-service-name"}, + } + + resolved := applyWorkloadTelemetryDefaults(envVars, "parquet") + + serviceName := mustFindEnvVar(t, resolved, "OTEL_SERVICE_NAME") + if serviceName.Value != "custom-service-name" { + t.Fatalf("expected explicit OTEL service name to be preserved, got %q", serviceName.Value) + } +} + +//func TestInjectManagedWorkloadTelemetryEnvvarsCoverage(t *testing.T) { +// scheme := runtime.NewScheme() +// if err := corev1.AddToScheme(scheme); err != nil { +// t.Fatalf("failed adding corev1 to scheme: %v", err) +// } +// client := fake.NewClientBuilder().WithScheme(scheme).Build() +// wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: "wandb-dev-v2", Namespace: "default"}} +// manifest := serverManifest.Manifest{} +// +// expectedApps := []string{ +// "api", +// "executor", +// "filemeta", +// "filestream", +// "flat-run-fields-updater", +// "glue", +// "metric-observer", +// "parquet", +// "weave", +// "weave-trace", +// "weave-trace-worker", +// "weave-trace-evaluate-model-worker", +// "nginx-proxy", +// } +// +// for _, appName := range expectedApps { +// t.Run(appName, func(t *testing.T) { +// envVars, err := injectManagedWorkloadTelemetryEnvvars( +// context.Background(), +// client, +// wandb, +// manifest, +// serverManifest.Application{Name: appName}, +// nil, +// true, +// ) +// if err != nil { +// t.Fatalf("injectManagedWorkloadTelemetryEnvvars returned error: %v", err) +// } +// +// for _, envName := range []string{ +// "OTEL_EXPORTER_OTLP_PROTOCOL", +// "OTEL_TRACES_EXPORTER", +// "OTEL_METRICS_EXPORTER", +// "OTEL_LOGS_EXPORTER", +// "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", +// "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", +// "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", +// "OTEL_SERVICE_NAME", +// "OTEL_RESOURCE_ATTRIBUTES", +// "GORILLA_TRACER", +// } { +// if !coreEnvVarSliceContains(envVars, envName) { +// t.Fatalf("expected managed telemetry env %q for application %q", envName, appName) +// } +// } +// }) +// } +// +// t.Run("ineligible-workload", func(t *testing.T) { +// envVars, err := injectManagedWorkloadTelemetryEnvvars( +// context.Background(), +// client, +// wandb, +// manifest, +// serverManifest.Application{Name: "anaconda2"}, +// nil, +// true, +// ) +// if err != nil { +// t.Fatalf("injectManagedWorkloadTelemetryEnvvars returned error: %v", err) +// } +// if len(envVars) != 0 { +// t.Fatalf("expected no managed telemetry envs for ineligible workload, got %#v", envVars) +// } +// }) +//} + +func TestInjectManagedWorkloadTelemetryEnvvarsDisabledSkipsInjection(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + client := fake.NewClientBuilder().WithScheme(scheme).Build() + wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: "wandb-dev-v2", Namespace: "default"}} + + envVars, err := injectManagedWorkloadTelemetryEnvvars( + context.Background(), + client, + wandb, + serverManifest.Manifest{}, + serverManifest.Application{Name: "api"}, + nil, + TelemetryRuntimeConfig{Enabled: false}, + ) + if err != nil { + t.Fatalf("injectManagedWorkloadTelemetryEnvvars returned error: %v", err) + } + if len(envVars) != 0 { + t.Fatalf("expected no managed telemetry envs when telemetry is disabled, got %#v", envVars) + } +} + +func TestInjectManagedWorkloadTelemetryEnvvarsIneligibleWorkloadSkipsInjection(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + client := fake.NewClientBuilder().WithScheme(scheme).Build() + wandb := telemetryStatusWandb("wandb-dev-v2", "default", "wandb-otel-connection") + + envVars, err := injectManagedWorkloadTelemetryEnvvars( + context.Background(), + client, + wandb, + serverManifest.Manifest{}, + serverManifest.Application{Name: "frontend"}, + nil, + TelemetryRuntimeConfig{Enabled: true}, + ) + if err != nil { + t.Fatalf("injectManagedWorkloadTelemetryEnvvars returned error: %v", err) + } + if len(envVars) != 0 { + t.Fatalf("expected no managed telemetry envs for ineligible workload, got %#v", envVars) + } +} + +func mustFindEnvVar(t *testing.T, envs []corev1.EnvVar, name string) corev1.EnvVar { + t.Helper() + for _, env := range envs { + if env.Name == name { + return env + } + } + t.Fatalf("env var %q not found", name) + return corev1.EnvVar{} +} + +func TestInjectManagedWorkloadTelemetryEnvvarsUsesStatusSecretName(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + client := fake.NewClientBuilder().WithScheme(scheme).Build() + wandb := telemetryStatusWandb("wandb-dev-v2", "default", "status-otel-secret") + + telemetryConfig := TelemetryRuntimeConfig{ + Enabled: true, + } + + envVars, err := injectManagedWorkloadTelemetryEnvvars( + context.Background(), + client, + wandb, + serverManifest.Manifest{}, + serverManifest.Application{Name: "api"}, + nil, + telemetryConfig, + ) + if err != nil { + t.Fatalf("injectManagedWorkloadTelemetryEnvvars returned error: %v", err) + } + + expectedNames := []string{ + "OTEL_EXPORTER_OTLP_PROTOCOL", + "OTEL_TRACES_EXPORTER", + "OTEL_METRICS_EXPORTER", + "OTEL_LOGS_EXPORTER", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "OTEL_SERVICE_NAME", + "OTEL_RESOURCE_ATTRIBUTES", + "GORILLA_TRACER", + "GORILLA_STATSD_ADDRESS", + } + + for _, name := range expectedNames { + env := mustFindEnvVar(t, envVars, name) + + if env.ValueFrom == nil { + t.Errorf("env var %q has no ValueFrom; expected SecretKeyRef to status-otel-secret", name) + continue + } + if env.ValueFrom.SecretKeyRef == nil { + t.Errorf("env var %q ValueFrom has no SecretKeyRef; expected reference to status-otel-secret", name) + continue + } + if got := env.ValueFrom.SecretKeyRef.Name; got != "status-otel-secret" { + t.Errorf("env var %q references secret %q, want status-otel-secret", name, got) + } + } +} + +func TestInjectManagedWorkloadTelemetryEnvvarsAddsDatadogAgentForDdtraceApps(t *testing.T) { + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed adding corev1 to scheme: %v", err) + } + client := fake.NewClientBuilder().WithScheme(scheme).Build() + wandb := telemetryStatusWandb("wandb-dev-v2", "default", "status-otel-secret") + + for _, appName := range []string{"anaconda2", "weave-trace"} { + t.Run(appName, func(t *testing.T) { + envVars, err := injectManagedWorkloadTelemetryEnvvars( + context.Background(), + client, + wandb, + serverManifest.Manifest{}, + serverManifest.Application{Name: appName}, + nil, + TelemetryRuntimeConfig{Enabled: true}, + ) + if err != nil { + t.Fatalf("injectManagedWorkloadTelemetryEnvvars returned error: %v", err) + } + + for _, name := range []string{"DD_TRACE_AGENT_URL", "DD_AGENT_HOST", "DD_TRACE_AGENT_PORT"} { + env := mustFindEnvVar(t, envVars, name) + if env.ValueFrom == nil || env.ValueFrom.SecretKeyRef == nil { + t.Fatalf("env var %q has no SecretKeyRef", name) + } + if got := env.ValueFrom.SecretKeyRef.Name; got != "status-otel-secret" { + t.Fatalf("env var %q references secret %q, want status-otel-secret", name, got) + } + } + + ddService := mustFindEnvVar(t, envVars, "DD_SERVICE") + if ddService.Value != appName { + t.Fatalf("unexpected DD_SERVICE value: %q", ddService.Value) + } + + for _, name := range []string{"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "GORILLA_TRACER", "GORILLA_STATSD_ADDRESS"} { + for _, env := range envVars { + if env.Name == name { + t.Fatalf("did not expect %q to be injected for ddtrace-only app", name) + } + } + } + }) + } +} + +func readyTelemetryResource(gvk schema.GroupVersionKind, name, namespace string) *unstructured.Unstructured { + return newTelemetryResource(gvk, name, namespace, []map[string]any{{"type": "Available", "status": "True"}}) +} + +func operationalTelemetryResource(gvk schema.GroupVersionKind, name, namespace string) *unstructured.Unstructured { + obj := newTelemetryResource(gvk, name, namespace, nil) + obj.Object["status"] = map[string]any{ + "updateStatus": "operational", + } + return obj +} + +func newTelemetryResource( + gvk schema.GroupVersionKind, + name string, + namespace string, + conditions []map[string]any, +) *unstructured.Unstructured { + obj := &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": gvk.GroupVersion().String(), + "kind": gvk.Kind, + "metadata": map[string]any{ + "name": name, + "namespace": namespace, + }, + }, + } + obj.SetGroupVersionKind(gvk) + if len(conditions) > 0 { + rawConditions := make([]any, 0, len(conditions)) + for _, condition := range conditions { + rawConditions = append(rawConditions, condition) + } + obj.Object["status"] = map[string]any{ + "conditions": rawConditions, + } + } + return obj +} + +func telemetryStatusWandb(name, namespace, secretName string) *apiv2.WeightsAndBiases { + return &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, + Status: apiv2.WeightsAndBiasesStatus{ + TelemetryStatus: apiv2.TelemetryInfraStatus{ + Connection: apiv2.TelemetryConnectionStatus{ + ConnectionSecret: secretName, + }, + }, + }, + } +} diff --git a/internal/controller/reconciler/v2_suite_test.go b/internal/controller/reconciler/v2_suite_test.go new file mode 100644 index 00000000..2141385c --- /dev/null +++ b/internal/controller/reconciler/v2_suite_test.go @@ -0,0 +1,30 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestV2Controllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "V2 Controller Suite") +} diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 25b10b1a..977aed74 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -18,21 +18,33 @@ package controller import ( "context" + "fmt" "os" "path/filepath" "testing" + mocov1beta2 "github.com/cybozu-go/moco/api/v1beta2" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - + apiv1 "github.com/wandb/operator/api/v1" + apiv2 "github.com/wandb/operator/api/v2" + webhookv2 "github.com/wandb/operator/internal/webhook/v2" + clickhousekeeperv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1" + clickhousev1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + argov1alpha1 "github.com/wandb/operator/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1" + redisv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redis/v1beta2" + redisreplicationv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redisreplication/v1beta2" + redissentinelv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/redissentinel/v1beta2" + seaweedv1 "github.com/wandb/operator/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - logf "sigs.k8s.io/controller-runtime/pkg/log" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" - - appsv1 "github.com/wandb/operator/api/v1" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/webhook" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" // +kubebuilder:scaffold:imports ) @@ -45,6 +57,7 @@ var ( testEnv *envtest.Environment cfg *rest.Config k8sClient client.Client + mgr manager.Manager ) func TestControllers(t *testing.T) { @@ -54,20 +67,65 @@ func TestControllers(t *testing.T) { } var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + ctrllog.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel = context.WithCancel(context.TODO()) var err error - err = appsv1.AddToScheme(scheme.Scheme) + + err = apiv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = apiv2.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = clickhousev1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = clickhousekeeperv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = seaweedv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = redisv1beta2.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = redisreplicationv1beta2.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = redissentinelv1beta2.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = argov1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = mocov1beta2.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = gatewayv1.Install(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + CRDInstallOptions: envtest.CRDInstallOptions{ + Paths: []string{ + filepath.Join("..", "..", "config", "crd", "bases"), + filepath.Join("..", "..", "pkg", "vendored", "altinity-clickhouse", "crds"), + filepath.Join("..", "..", "pkg", "vendored", "seaweedfs-operator", "crds"), + filepath.Join("..", "..", "pkg", "vendored", "moco", "crds"), + filepath.Join("..", "..", "pkg", "vendored", "redis-operator", "crds"), + filepath.Join("..", "..", "pkg", "vendored", "argo-rollouts", "crds"), + filepath.Join("..", "..", "pkg", "vendored", "gateway-api", "crds"), + }, + }, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "config", "webhook")}, + }, ErrorIfCRDPathMissing: true, + Scheme: scheme.Scheme, } // Retrieve the first found binary directory to allow running tests from IDEs @@ -77,12 +135,41 @@ var _ = BeforeSuite(func() { // cfg is defined in this file globally. cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) + + mgr, err = manager.New(cfg, manager.Options{ + Scheme: scheme.Scheme, + WebhookServer: webhook.NewServer(webhook.Options{ + Host: testEnv.WebhookInstallOptions.LocalServingHost, + Port: testEnv.WebhookInstallOptions.LocalServingPort, + CertDir: testEnv.WebhookInstallOptions.LocalServingCertDir, + }), + }) + Expect(err).NotTo(HaveOccurred()) + + err = webhookv2.SetupWeightsAndBiasesWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + err = webhookv2.SetupApplicationWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + go func() { + err = mgr.Start(ctx) + if err != nil && ctx.Err() == nil { + // Only fail if not cancelled + Fail(fmt.Sprintf("manager failed to start: %v", err)) + } + }() + + // Wait for webhook server to be ready + Eventually(func() error { + return mgr.GetWebhookServer().StartedChecker()(nil) + }).Should(Succeed()) }) var _ = AfterSuite(func() { @@ -104,7 +191,7 @@ func getFirstFoundEnvTestBinaryDir() string { basePath := filepath.Join("..", "..", "bin", "k8s") entries, err := os.ReadDir(basePath) if err != nil { - logf.Log.Error(err, "Failed to read directory", "path", basePath) + ctrllog.Log.Error(err, "Failed to read directory", "path", basePath) return "" } for _, entry := range entries { diff --git a/internal/controller/weightsandbiases_controller.go b/internal/controller/weightsandbiases_controller.go index 76396eb2..965264ee 100644 --- a/internal/controller/weightsandbiases_controller.go +++ b/internal/controller/weightsandbiases_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2023. +Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,65 +18,77 @@ package controller import ( "context" - "reflect" - "time" + "fmt" - rbacv1 "k8s.io/api/rbac/v1" - - "github.com/wandb/operator/pkg/wandb/spec/state" - appsv1 "k8s.io/api/apps/v1" + mocov1beta2 "github.com/cybozu-go/moco/api/v1beta2" + apiv2 "github.com/wandb/operator/api/v2" + v2 "github.com/wandb/operator/internal/controller/reconciler" + "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/pkg/wandb/spec/channel/deployer" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" - - corev1 "k8s.io/api/core/v1" - - apiv1 "github.com/wandb/operator/api/v1" - "github.com/wandb/operator/internal/controller/ctrlqueue" - "github.com/wandb/operator/pkg/wandb/spec" - "github.com/wandb/operator/pkg/wandb/spec/channel/deployer" - "github.com/wandb/operator/pkg/wandb/spec/operator" - "github.com/wandb/operator/pkg/wandb/spec/state/secrets" - "github.com/wandb/operator/pkg/wandb/spec/utils" - "github.com/wandb/operator/pkg/wandb/status" - - "k8s.io/apimachinery/pkg/api/errors" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" ) -const resFinalizer = "finalizer.app.wandb.com" - // WeightsAndBiasesReconciler reconciles a WeightsAndBiases object type WeightsAndBiasesReconciler struct { client.Client - IsAirgapped bool - DeployerClient deployer.DeployerInterface - Scheme *runtime.Scheme - Recorder record.EventRecorder - DryRun bool - Debug bool + IsAirgapped bool + DeployerClient deployer.DeployerInterface + Scheme *runtime.Scheme + Recorder record.EventRecorder + DryRun bool + Debug bool + EnableV2 bool + TelemetryConfigRef types.NamespacedName } -//+kubebuilder:rbac:groups=apps.wandb.com,resources=weightsandbiases,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=apps.wandb.com,resources=weightsandbiases/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=apps.wandb.com,resources=weightsandbiases/finalizers,verbs=update //+kubebuilder:rbac:groups="",resources=configmaps;events;persistentvolumeclaims;secrets;serviceaccounts;services,verbs=update;delete;get;list;create;patch;watch -//+kubebuilder:rbac:groups="",resources=endpoints;ingresses;nodes;nodes/spec;nodes/stats;nodes/metrics;nodes/proxy;namespaces;namespaces/status;replicationcontrollers;replicationcontrollers/status;resourcequotas;pods;pods/log;pods/status,verbs=get;list;watch -//+kubebuilder:rbac:groups=apps,resources=deployments;controllerrevisions;daemonsets;replicasets;statefulsets,verbs=update;delete;get;list;create;patch;watch +//+kubebuilder:rbac:groups="",resources=endpoints;nodes;nodes/spec;nodes/stats;nodes/metrics;nodes/proxy;namespaces;namespaces/status;replicationcontrollers;replicationcontrollers/status;resourcequotas;pods;pods/log;pods/status,verbs=get;list;watch //+kubebuilder:rbac:groups=apps,resources=deployments/status;daemonsets/status;replicasets/status;statefulsets/status,verbs=get +//+kubebuilder:rbac:groups=apps,resources=deployments;controllerrevisions;daemonsets;replicasets;statefulsets,verbs=update;delete;get;list;create;patch;watch +//+kubebuilder:rbac:groups=apps.wandb.com,resources=weightsandbiases,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=apps.wandb.com,resources=weightsandbiases/finalizers,verbs=update +//+kubebuilder:rbac:groups=apps.wandb.com,resources=weightsandbiases/status,verbs=get;update;patch //+kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=update;delete;get;list;patch;create;watch -//+kubebuilder:rbac:groups=batch,resources=cronjobs;jobs,verbs=get;list;watch;create;delete;patch -//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=list;watch +//+kubebuilder:rbac:groups=batch,resources=cronjobs;jobs,verbs=get;list;watch;create;delete;update;patch +//+kubebuilder:rbac:groups=clickhouse.altinity.com,resources=clickhouseinstallations,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=clickhouse.altinity.com,resources=clickhouseinstallations/status,verbs=get +//+kubebuilder:rbac:groups=clickhouse-keeper.altinity.com,resources=clickhousekeeperinstallations,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=clickhouse-keeper.altinity.com,resources=clickhousekeeperinstallations/status,verbs=get //+kubebuilder:rbac:groups=cloud.google.com,resources=backendconfigs,verbs=update;delete;get;list;patch;create;watch -//+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses;ingresses/status;networkpolicies,verbs=update;delete;get;list;create;patch;watch +//+kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=list;watch +//+kubebuilder:rbac:groups=grafana.integreatly.org,resources=grafanas;grafanadashboards;grafanadatasources,verbs=get;list;watch +//+kubebuilder:rbac:groups=grafana.integreatly.org,resources=grafanas/status;grafanadashboards/status;grafanadatasources/status,verbs=get +//+kubebuilder:rbac:groups=seaweed.seaweedfs.com,resources=seaweeds,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=seaweed.seaweedfs.com,resources=seaweeds/status,verbs=get +//+kubebuilder:rbac:groups=moco.cybozu.com,resources=mysqlclusters,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=moco.cybozu.com,resources=mysqlclusters/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=operator.victoriametrics.com,resources=vmagents;vmalerts;vmnodescrapes;vmpodscrapes;vmrules;vmservicescrapes;vmsingles;vlsingles;vtsingles,verbs=get;list;watch +//+kubebuilder:rbac:groups=operator.victoriametrics.com,resources=vmagents/status;vmalerts/status;vmsingles/status;vlsingles/status;vtsingles/status,verbs=get +//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways;httproutes;backendtlspolicies,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways/status;backendtlspolicies/status,verbs=get +//+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses;networkpolicies,verbs=update;delete;get;list;create;patch;watch +//+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses/status,verbs=get;list;watch +//+kubebuilder:rbac:groups=gateway.nginx.org,resources=clientsettingspolicies,verbs=update;delete;get;list;create;patch;watch +//+kubebuilder:rbac:groups=networking.gke.io,resources=healthcheckpolicies,verbs=update;delete;get;list;create;patch;watch //+kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=update;delete;get;list;patch;create;watch //+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;rolebindings;clusterroles;clusterrolebindings,verbs=update;delete;get;list;patch;create;watch +//+kubebuilder:rbac:groups=redis.redis.opstreelabs.in,resources=redis;redissentinels;redisreplications,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=redis.redis.opstreelabs.in,resources=redis/status,verbs=get +//+kubebuilder:rbac:groups=security.openshift.io,resources=securitycontextconstraints,resourceNames=nonroot-v2,verbs=use //+kubebuilder:rbac:urls=/metrics,verbs=get // Deprecated/Erroneously required RBAC rules @@ -90,332 +102,145 @@ type WeightsAndBiasesReconciler struct { func (r *WeightsAndBiasesReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := ctrllog.FromContext(ctx) log.Info( - "=== Reconciling Weights & Biases instance...", + "=== Reconciling Weights & Biases V2 instance...", "NamespacedName", req.NamespacedName, "Name", req.Name, "start", true, ) - wandb := &apiv1.WeightsAndBiases{} - if err := r.Client.Get(ctx, req.NamespacedName, wandb); err != nil { - if errors.IsNotFound(err) { - return ctrlqueue.DoNotRequeue() - } - return ctrlqueue.RequeueWithError(err) - } - - log.Info( - "Found Weights & Biases instance, processing the spec...", - "Spec", wandb.Spec, - ) - - r.Recorder.Event(wandb, corev1.EventTypeNormal, "Reconciling", "Reconciling") - - statusManager := status.NewManager(ctx, r.Client, wandb) - configMapState := secrets.New(ctx, r.Client, wandb, r.Scheme) - specManager := state.New(ctx, r.Client, wandb, r.Scheme, configMapState) - - r.Recorder.Event(wandb, corev1.EventTypeNormal, "LoadingConfig", "Loading desired configuration") + wandb := &apiv2.WeightsAndBiases{} - userInputSpec, err := specManager.GetUserInput() - if errors.IsNotFound(err) { - log.Info("No user input spec found, creating a new one") - userInputSpec = &spec.Spec{Values: map[string]interface{}{}} - err = specManager.SetUserInput(userInputSpec) - if err != nil { - return ctrlqueue.RequeueWithError(err) + if err := r.Get(ctx, req.NamespacedName, wandb); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil } - } else if err != nil { - log.Error(err, "error retrieving user input spec") - return ctrlqueue.RequeueWithError(err) + return ctrl.Result{}, err } - var releaseID string - if releaseIDValue, ok := userInputSpec.Values["_releaseId"].(string); ok { - releaseID = releaseIDValue - log.Info("Version Pinning is enabled", "releaseId:", releaseID) - } - - crdSpec := operator.Spec(wandb) - - currentActiveSpec, err := specManager.GetActive() + telemetryConfig, err := r.loadTelemetryConfig(ctx) if err != nil { - // This scenario can happen if we have not successfully deployed in the - // past. - log.Info("No active spec found.") + log.Error(err, "failed to load telemetry configuration", "configMap", r.TelemetryConfigRef) + return ctrl.Result{}, err } - license := utils.GetLicense(ctx, r.Client, wandb, crdSpec, userInputSpec) - - var deployerSpec *spec.Spec - if !r.IsAirgapped { - deployerSpec, err = r.DeployerClient.GetSpec(deployer.GetSpecOptions{ - License: license, - ActiveState: currentActiveSpec, - ReleaseId: releaseID, - Debug: r.Debug, - }) - if err != nil { - log.Info("Failed to get spec from deployer", "error", err) - // This scenario may occur if the user disables networking, or if the deployer - // is not operational, and a version has been deployed successfully. Rather than - // reverting to the container defaults, we've stored the most recent successful - // deployer release in the cache - // Attempt to retrieve the cached release - if deployerSpec, err = specManager.Get("latest-cached-release"); err != nil { - log.Info("No cached release found", "error", err.Error()) - } - if r.Debug { - log.Info("Using cached deployer spec", "spec", deployerSpec.SensitiveValuesMasked()) - } - } - - if deployerSpec != nil { - if r.Debug { - log.Info("Writing deployer spec to cache", "spec", deployerSpec.SensitiveValuesMasked()) - } - if err := specManager.Set("latest-cached-release", deployerSpec); err != nil { - r.Recorder.Event(wandb, corev1.EventTypeNormal, "SecretWriteFailed", "Unable to write secret to kubernetes") - log.Error(err, "Unable to save latest release.") - return ctrlqueue.DoNotRequeue() - } - } - } - - desiredSpec := new(spec.Spec) + return v2.Reconcile(ctx, r.Client, r.Recorder, wandb, telemetryConfig) +} - if r.Debug { - log.Info("Initial desired spec", "spec", desiredSpec.SensitiveValuesMasked()) - } +// Delete was taken from original v1 reconciler +func (r *WeightsAndBiasesReconciler) Delete(e event.DeleteEvent) bool { + return !e.DeleteStateUnknown +} - // First takes precedence - if err := desiredSpec.Merge(crdSpec); err != nil { - log.Error(err, "Failed to merge CRD spec into desired spec") - return ctrlqueue.RequeueWithError(err) - } - if r.Debug { - log.Info("Desired spec after merging crdSpec", "spec", desiredSpec.SensitiveValuesMasked()) - } +// SetupWithManager sets up the controller with the Manager. +func (r *WeightsAndBiasesReconciler) SetupWithManager(mgr ctrl.Manager) error { + var b = ctrl.NewControllerManagedBy(mgr). + For(&apiv2.WeightsAndBiases{}). + // Applications carry plain (non-controller) owner refs; without + // MatchEveryOwner this watch never fires and app status changes stop + // refreshing status.wandb.applications once the estate settles. + Owns(&apiv2.Application{}, builder.MatchEveryOwner). + Owns(&batchv1.Job{}). + Owns(&corev1.Secret{}). + Owns(&corev1.ConfigMap{}). + Owns(&networkingv1.Ingress{}) + if utils.IsRegistered(r.Scheme, &gatewayv1.Gateway{}) { + b = b.Watches(&gatewayv1.Gateway{}, handler.EnqueueRequestsFromMapFunc(r.mapGatewayToWandb)) + } + if utils.IsRegistered(r.Scheme, &mocov1beta2.MySQLCluster{}) { + b = b.Owns(&mocov1beta2.MySQLCluster{}) + } + if r.TelemetryConfigRef.Name != "" { + b = b.Watches( + &corev1.ConfigMap{}, + handler.EnqueueRequestsFromMapFunc(r.mapTelemetryConfigToWandb), + builder.WithPredicates(predicate.NewPredicateFuncs(r.isTelemetryConfig)), + ) + } + + return b.Complete(r) +} - if err := desiredSpec.Merge(userInputSpec); err != nil { - log.Error(err, "Failed to merge user input spec into desired spec") - return ctrlqueue.RequeueWithError(err) - } - if r.Debug { - log.Info("Desired spec after merging userInputSpec", "spec", desiredSpec.SensitiveValuesMasked()) +func (r *WeightsAndBiasesReconciler) loadTelemetryConfig(ctx context.Context) (v2.TelemetryRuntimeConfig, error) { + if r.TelemetryConfigRef.Name == "" { + return v2.DefaultTelemetryRuntimeConfig(), nil } - if err := desiredSpec.Merge(deployerSpec); err != nil { - log.Error(err, "Failed to merge deployer spec into desired spec") - return ctrlqueue.RequeueWithError(err) - } + return v2.LoadTelemetryRuntimeConfigFromConfigMap( + ctx, + r.Client, + r.TelemetryConfigRef, + v2.DefaultTelemetryRuntimeConfig(), + ) +} - if r.Debug { - log.Info("Desired spec after merging deployerSpec", "spec", desiredSpec.SensitiveValuesMasked()) - } +func (r *WeightsAndBiasesReconciler) isTelemetryConfig(obj client.Object) bool { + return client.ObjectKeyFromObject(obj) == r.TelemetryConfigRef +} - if err := desiredSpec.Merge(operator.Defaults(wandb, r.Scheme)); err != nil { - log.Error(err, "Failed to merge operator defaults into desired spec") - return ctrlqueue.RequeueWithError(err) +func (r *WeightsAndBiasesReconciler) mapTelemetryConfigToWandb(ctx context.Context, obj client.Object) []ctrl.Request { + if !r.isTelemetryConfig(obj) { + return nil } - if r.Debug { - log.Info("Desired spec after merging operator defaults", "spec", desiredSpec.SensitiveValuesMasked()) - } - - log.Info("Desired spec", "spec", desiredSpec.SensitiveValuesMasked()) - - hasNotBeenFlaggedForDeletion := wandb.ObjectMeta.DeletionTimestamp.IsZero() - if hasNotBeenFlaggedForDeletion { - if currentActiveSpec != nil { - log.Info("Active spec found", "spec", currentActiveSpec.SensitiveValuesMasked()) - if currentActiveSpec.IsEqual(desiredSpec) { - log.Info("No changes found") - statusManager.Set(status.Completed) - return ctrlqueue.Requeue(desiredSpec) - } else { - diff := currentActiveSpec.DiffValues(desiredSpec) - log.Info("Changes found", "diff", diff) - } - } - - if desiredSpec.Chart == nil { - statusManager.Set(status.InvalidConfig) - log.Error(err, "No release type was found in the spec") - return ctrlqueue.Requeue(desiredSpec) - } - t := reflect.TypeOf(desiredSpec.Chart) - typ := t.Name() - if t.Kind() == reflect.Ptr { - typ = "*" + t.Elem().Name() - } - log.Info("Found release type "+typ, "release", reflect.TypeOf(desiredSpec.Chart)) - - statusManager.Set(status.Loading) - - if !ctrlqueue.ContainsString(wandb.GetFinalizers(), resFinalizer) { - wandb.ObjectMeta.Finalizers = append(wandb.ObjectMeta.Finalizers, resFinalizer) - if err := r.Client.Update(ctx, wandb); err != nil { - return ctrlqueue.Requeue(desiredSpec) - } - } - - log.Info("Applying spec...", "spec", desiredSpec.SensitiveValuesMasked()) - if !r.DryRun { - if err := desiredSpec.Apply(ctx, r.Client, wandb, r.Scheme); err != nil { - statusManager.Set(status.InvalidConfig) - r.Recorder.Event(wandb, corev1.EventTypeNormal, "ApplyFailed", "Invalid config for apply") - log.Error(err, "Failed to apply config changes.") - return ctrlqueue.Requeue(desiredSpec) - } - } - log.Info("Successfully applied spec", "spec", desiredSpec.SensitiveValuesMasked()) - - if err := specManager.SetActive(desiredSpec); err != nil { - r.Recorder.Event(wandb, corev1.EventTypeNormal, "SetActiveFailed", "Failed to save active state") - log.Error(err, "Failed to save active successful spec.") - statusManager.Set(status.InvalidConfig) - return ctrlqueue.Requeue(desiredSpec) - } - if r.Debug { - log.Info("Successfully saved active spec", "spec", desiredSpec.SensitiveValuesMasked()) - } - r.Recorder.Event(wandb, corev1.EventTypeNormal, "Completed", "Completed reconcile successfully") - if err := r.discoverAndPatchResources(ctx, wandb); err != nil { - log.Error(err, "Failed to discover and patch resources") - return ctrlqueue.Requeue(desiredSpec) - } - statusManager.Set(status.Completed) - - return ctrlqueue.Requeue(desiredSpec) + wandbList := &apiv2.WeightsAndBiasesList{} + if err := r.List(ctx, wandbList); err != nil { + return nil } - if ctrlqueue.ContainsString(wandb.ObjectMeta.Finalizers, resFinalizer) { - if desiredSpec.Chart != nil { - log.Info("Deprovisioning", "release", reflect.TypeOf(desiredSpec.Chart)) - if !r.DryRun { - if err := desiredSpec.Prune(ctx, r.Client, wandb, r.Scheme); err != nil { - log.Error(err, "Failed to cleanup deployment.") - return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil - } else { - log.Info("Successfully cleaned up resources") - } - } - } - - controllerutil.RemoveFinalizer(wandb, resFinalizer) - r.Client.Update(ctx, wandb) + requests := make([]ctrl.Request, 0, len(wandbList.Items)) + for i := range wandbList.Items { + requests = append(requests, ctrl.Request{ + NamespacedName: client.ObjectKeyFromObject(&wandbList.Items[i]), + }) } - return ctrlqueue.DoNotRequeue() + return requests } -func (r *WeightsAndBiasesReconciler) discoverAndPatchResources(ctx context.Context, wandb *apiv1.WeightsAndBiases) error { - log := ctrllog.FromContext(ctx) - var managedResources []client.Object - resourceKinds := []struct { - name string - list client.ObjectList - }{ - {"Deployment", &appsv1.DeploymentList{}}, - {"StatefulSet", &appsv1.StatefulSetList{}}, - {"Ingress", &networkingv1.IngressList{}}, - {"DaemonSet", &appsv1.DaemonSetList{}}, - {"Service", &corev1.ServiceList{}}, - {"ConfigMap", &corev1.ConfigMapList{}}, - {"Secret", &corev1.SecretList{}}, - {"Role", &rbacv1.RoleList{}}, - {"RoleBinding", &rbacv1.RoleBindingList{}}, +func (r *WeightsAndBiasesReconciler) mapGatewayToWandb(ctx context.Context, obj client.Object) []ctrl.Request { + gateway, ok := obj.(*gatewayv1.Gateway) + if !ok { + return nil } - // Discover resources - for _, resourceKind := range resourceKinds { - log.Info("Fetching resources managed by Helm chart 'wandb'", "kind", resourceKind.name) - - labels := client.MatchingLabels{ - "app.kubernetes.io/managed-by": "Helm", - "app.kubernetes.io/instance": wandb.ObjectMeta.Name, - } - if err := r.Client.List(ctx, resourceKind.list, client.InNamespace(wandb.Namespace), labels); err != nil { - log.Error(err, "Failed to list resources", "kind", resourceKind.name) - continue - } - - items := reflect.ValueOf(resourceKind.list).Elem().FieldByName("Items") - for i := 0; i < items.Len(); i++ { - resource := items.Index(i).Addr().Interface().(client.Object) - managedResources = append(managedResources, resource) - log.Info("Found resource", "name", resource.GetName(), "kind", resourceKind.name) - } + wandbList := &apiv2.WeightsAndBiasesList{} + if err := r.List(ctx, wandbList); err != nil { + return nil } - // Add owner references to discovered resources - for _, resource := range managedResources { - if err := controllerutil.SetOwnerReference(wandb, resource, r.Scheme); err != nil { - log.Error(err, "Failed to set owner reference", "resource", resource.GetName(), "kind", resource.GetObjectKind().GroupVersionKind().Kind) + requests := make([]ctrl.Request, 0) + for i := range wandbList.Items { + wandb := &wandbList.Items[i] + if !gatewayMatchesWandb(gateway, wandb) { continue } - if err := r.Client.Update(ctx, resource); err != nil { - log.Error(err, "Failed to update resource with owner reference", "resource", resource.GetName(), "kind", resource.GetObjectKind().GroupVersionKind().Kind) - } else { - log.Info("Owner reference added successfully", "resource", resource.GetName(), "kind", resource.GetObjectKind().GroupVersionKind().Kind) - } + requests = append(requests, ctrl.Request{ + NamespacedName: client.ObjectKeyFromObject(wandb), + }) } - return nil -} - -func (r *WeightsAndBiasesReconciler) Delete(e event.DeleteEvent) bool { - return !e.DeleteStateUnknown -} - -// SetupWithManager sets up the controller with the Manager. -func (r *WeightsAndBiasesReconciler) SetupWithManager(mgr ctrl.Manager) error { - builder := ctrl.NewControllerManagedBy(mgr). - For(&apiv1.WeightsAndBiases{}, builder.WithPredicates(filterWBEvents{})). - Owns(&corev1.Secret{}, builder.WithPredicates(filterSecretEvents{})). - Owns(&corev1.ConfigMap{}) - return builder.Complete(r) -} - -type filterWBEvents struct { - predicate.Funcs -} - -func (filterWBEvents) Update(e event.UpdateEvent) bool { - // Checking whether the Object's Generation has changed. If it has not - // (indicating a non-spec change), it returns false - thus ignoring the - // event. - return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() -} - -func (filterWBEvents) Create(e event.CreateEvent) bool { - return true -} -func (filterWBEvents) Delete(e event.DeleteEvent) bool { - return true + return requests } -func (filterWBEvents) Generic(e event.GenericEvent) bool { - return false -} - -type filterSecretEvents struct { - predicate.Funcs -} +func gatewayMatchesWandb(gateway *gatewayv1.Gateway, wandb *apiv2.WeightsAndBiases) bool { + if wandb.Spec.Networking.Mode != apiv2.NetworkingModeGatewayAPI || wandb.Spec.Networking.GatewayAPI == nil { + return false + } -func (filterSecretEvents) Update(e event.UpdateEvent) bool { - return true -} + if wandb.Spec.Networking.GatewayAPI.Gateway.Managed { + return gateway.Namespace == wandb.Namespace && + gateway.Name == fmt.Sprintf("%s-gateway", wandb.Name) + } -func (filterSecretEvents) Create(e event.CreateEvent) bool { - return false -} + ref := wandb.Spec.Networking.GatewayAPI.Gateway.GatewayRef + if ref == nil { + return false + } -func (filterSecretEvents) Delete(e event.DeleteEvent) bool { - return true -} + namespace := ref.Namespace + if namespace == "" { + namespace = wandb.Namespace + } -func (filterSecretEvents) Generic(e event.GenericEvent) bool { - return false + return gateway.Namespace == namespace && gateway.Name == ref.Name } diff --git a/internal/controller/weightsandbiases_controller_networking_test.go b/internal/controller/weightsandbiases_controller_networking_test.go new file mode 100644 index 00000000..e8da2ee3 --- /dev/null +++ b/internal/controller/weightsandbiases_controller_networking_test.go @@ -0,0 +1,363 @@ +package controller + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + v2 "github.com/wandb/operator/internal/controller/reconciler" + "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +var _ = Describe("WeightsAndBiases Networking", func() { + const wandbNamespace = "default" + + It("reconciles a managed Gateway and cross-namespace infra HTTPRoutes", func() { + ctx := context.Background() + wandbName := "network-gateway-managed" + infraNamespace := "network-gateway-managed-infra" + listenerName := "https" + gatewayClassName := "test-gateway-class" + + createNamespaceIfMissing(ctx, infraNamespace) + wandb, objectStoreService := newNetworkingWandb(wandbName, infraNamespace) + wandb.Spec.Networking = apiv2.NetworkingSpec{ + Mode: apiv2.NetworkingModeGatewayAPI, + GatewayAPI: &apiv2.GatewayAPIConfig{ + ListenerName: &listenerName, + Gateway: apiv2.GatewayConfig{ + Managed: true, + GatewayClassName: &gatewayClassName, + Listeners: []apiv2.GatewayListener{{ + Name: listenerName, + Port: 80, + Protocol: string(gatewayv1.HTTPProtocolType), + }}, + }, + }, + } + + Expect(k8sClient.Create(ctx, wandb)).To(Succeed()) + Expect(k8sClient.Create(ctx, objectStoreService)).To(Succeed()) + DeferCleanup(deleteIfPresent, ctx, wandb) + + wandb = markWandbReadyForNetworking(ctx, wandbName, wandbNamespace) + reconcileNetworkingManifest(ctx, wandb) + + gatewayName := fmt.Sprintf("%s-gateway", wandbName) + gateway := &gatewayv1.Gateway{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: gatewayName, Namespace: wandbNamespace}, gateway)).To(Succeed()) + Expect(gateway.Spec.Listeners).To(HaveLen(1)) + Expect(gateway.Spec.Listeners[0].AllowedRoutes).NotTo(BeNil()) + Expect(gateway.Spec.Listeners[0].AllowedRoutes.Namespaces).NotTo(BeNil()) + Expect(gateway.Spec.Listeners[0].AllowedRoutes.Namespaces.From).NotTo(BeNil()) + Expect(*gateway.Spec.Listeners[0].AllowedRoutes.Namespaces.From).To(Equal(gatewayv1.NamespacesFromAll)) + + infraRoute := &gatewayv1.HTTPRoute{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: fmt.Sprintf("%s-bucket-default", wandbName), + Namespace: infraNamespace, + }, infraRoute)).To(Succeed()) + Expect(infraRoute.Spec.ParentRefs).To(HaveLen(1)) + Expect(infraRoute.Spec.ParentRefs[0].Name).To(Equal(gatewayv1.ObjectName(gatewayName))) + Expect(infraRoute.Spec.ParentRefs[0].Namespace).NotTo(BeNil()) + Expect(*infraRoute.Spec.ParentRefs[0].Namespace).To(Equal(gatewayv1.Namespace(wandbNamespace))) + + gateway.Status.Addresses = []gatewayv1.GatewayStatusAddress{{Value: "10.0.0.5"}} + gateway.Status.Conditions = []metav1.Condition{{ + Type: string(gatewayv1.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + Reason: string(gatewayv1.GatewayReasonProgrammed), + LastTransitionTime: metav1.Now(), + }} + Expect(k8sClient.Status().Update(ctx, gateway)).To(Succeed()) + + wandb = getWandb(ctx, wandbName, wandbNamespace) + reconcileNetworkingManifest(ctx, wandb) + wandb = getWandb(ctx, wandbName, wandbNamespace) + + Expect(wandb.Status.GatewayStatus).NotTo(BeNil()) + Expect(wandb.Status.GatewayStatus.GatewayRef).NotTo(BeNil()) + Expect(wandb.Status.GatewayStatus.GatewayRef.Name).To(Equal(gatewayName)) + Expect(wandb.Status.GatewayStatus.Ready).To(BeTrue()) + Expect(wandb.Status.GatewayStatus.Addresses).To(ContainElement("10.0.0.5")) + }) + + It("reconciles applications against an external Gateway reference", func() { + ctx := context.Background() + wandbName := "network-gateway-external" + gatewayNamespace := "network-gateway-external-shared" + gatewayName := "shared-gateway" + + createNamespaceIfMissing(ctx, gatewayNamespace) + externalGateway := &gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: gatewayName, + Namespace: gatewayNamespace, + }, + Spec: gatewayv1.GatewaySpec{ + GatewayClassName: gatewayv1.ObjectName("shared-class"), + Listeners: []gatewayv1.Listener{{ + Name: gatewayv1.SectionName("http"), + Port: 80, + Protocol: gatewayv1.HTTPProtocolType, + }}, + }, + } + Expect(k8sClient.Create(ctx, externalGateway)).To(Succeed()) + DeferCleanup(deleteIfPresent, ctx, externalGateway) + + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: gatewayName, Namespace: gatewayNamespace}, externalGateway)).To(Succeed()) + wandb, objectStoreService := newNetworkingWandb(wandbName, "") + wandb.Spec.Networking = apiv2.NetworkingSpec{ + Mode: apiv2.NetworkingModeGatewayAPI, + GatewayAPI: &apiv2.GatewayAPIConfig{ + Gateway: apiv2.GatewayConfig{ + Managed: false, + GatewayRef: &apiv2.GatewayReference{ + Name: gatewayName, + Namespace: gatewayNamespace, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, wandb)).To(Succeed()) + Expect(k8sClient.Create(ctx, objectStoreService)).To(Succeed()) + DeferCleanup(deleteIfPresent, ctx, wandb) + + externalGateway.Status.Addresses = []gatewayv1.GatewayStatusAddress{{Value: "10.0.0.6"}} + externalGateway.Status.Conditions = []metav1.Condition{{ + Type: string(gatewayv1.GatewayConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(gatewayv1.GatewayReasonAccepted), + LastTransitionTime: metav1.Now(), + }} + Expect(k8sClient.Status().Update(ctx, externalGateway)).To(Succeed()) + + wandb = markWandbReadyForNetworking(ctx, wandbName, wandbNamespace) + reconcileNetworkingManifest(ctx, wandb) + wandb = getWandb(ctx, wandbName, wandbNamespace) + + managedGateway := &gatewayv1.Gateway{} + err := k8sClient.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("%s-gateway", wandbName), Namespace: wandbNamespace}, managedGateway) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + + Expect(wandb.Status.GatewayStatus).NotTo(BeNil()) + Expect(wandb.Status.GatewayStatus.GatewayRef).NotTo(BeNil()) + Expect(wandb.Status.GatewayStatus.GatewayRef.Name).To(Equal(gatewayName)) + Expect(wandb.Status.GatewayStatus.GatewayRef.Namespace).To(Equal(gatewayNamespace)) + Expect(wandb.Status.GatewayStatus.Ready).To(BeTrue()) + Expect(wandb.Status.GatewayStatus.Addresses).To(ContainElement("10.0.0.6")) + + app := &apiv2.Application{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "api", Namespace: wandbNamespace}, app)).To(Succeed()) + Expect(app.Spec.HTTPRouteTemplate).NotTo(BeNil()) + Expect(app.Spec.HTTPRouteTemplate.ParentRefs).To(HaveLen(1)) + Expect(app.Spec.HTTPRouteTemplate.ParentRefs[0].Name).To(Equal(gatewayv1.ObjectName(gatewayName))) + Expect(app.Spec.HTTPRouteTemplate.ParentRefs[0].Namespace).NotTo(BeNil()) + Expect(*app.Spec.HTTPRouteTemplate.ParentRefs[0].Namespace).To(Equal(gatewayv1.Namespace(gatewayNamespace))) + }) + + It("reconciles a consolidated Ingress and mirrors its load balancer status", func() { + ctx := context.Background() + wandbName := "network-ingress" + ingressClassName := "nginx" + + wandb, service := newNetworkingWandb(wandbName, "") + wandb.Spec.Networking = apiv2.NetworkingSpec{ + Mode: apiv2.NetworkingModeIngress, + Ingress: &apiv2.IngressConfig{ + IngressClassName: &ingressClassName, + }, + TLS: &apiv2.TLSConfig{ + SecretName: "wandb-tls", + }, + Annotations: map[string]string{ + "example.com/ingress": "enabled", + }, + } + Expect(k8sClient.Create(ctx, wandb)).To(Succeed()) + Expect(k8sClient.Create(ctx, service)).To(Succeed()) + DeferCleanup(deleteIfPresent, ctx, wandb) + + wandb = markWandbReadyForNetworking(ctx, wandbName, wandbNamespace) + reconcileNetworkingManifest(ctx, wandb) + + ingress := &networkingv1.Ingress{} + Expect(k8sClient.Get(ctx, types.NamespacedName{ + Name: wandbName, + Namespace: wandbNamespace, + }, ingress)).To(Succeed()) + Expect(ingress.Spec.IngressClassName).NotTo(BeNil()) + Expect(*ingress.Spec.IngressClassName).To(Equal(ingressClassName)) + Expect(ingress.Annotations).To(HaveKeyWithValue("example.com/ingress", "enabled")) + Expect(ingress.Spec.TLS).To(HaveLen(1)) + Expect(ingress.Spec.TLS[0].SecretName).To(Equal("wandb-tls")) + Expect(ingress.Spec.Rules).NotTo(BeEmpty()) + + ingress.Status.LoadBalancer.Ingress = []networkingv1.IngressLoadBalancerIngress{{ + IP: "34.118.10.1", + }} + Expect(k8sClient.Status().Update(ctx, ingress)).To(Succeed()) + + wandb = getWandb(ctx, wandbName, wandbNamespace) + reconcileNetworkingManifest(ctx, wandb) + wandb = getWandb(ctx, wandbName, wandbNamespace) + + Expect(wandb.Status.IngressStatus).NotTo(BeNil()) + Expect(wandb.Status.IngressStatus.Name).To(Equal(wandbName)) + Expect(wandb.Status.IngressStatus.LoadBalancerIngress).To(HaveLen(1)) + Expect(wandb.Status.IngressStatus.LoadBalancerIngress[0].IP).To(Equal("34.118.10.1")) + }) +}) + +func newNetworkingWandb(name string, infraNamespace string) (*apiv2.WeightsAndBiases, *corev1.Service) { + internalServiceAuthEnabled := false + if infraNamespace == "" { + infraNamespace = "default" + } + + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Spec: apiv2.WeightsAndBiasesSpec{ + Size: apiv2.SizeDev, + Wandb: apiv2.WandbAppSpec{ + Hostname: "http://localhost", + Features: map[string]bool{}, + ManifestRepository: manifestsRepository, + Version: "0.83.0-clickhouse-keeper.2", + InternalServiceAuth: apiv2.InternalServiceAuth{ + Enabled: &internalServiceAuthEnabled, + }, + }, + RetentionPolicy: apiv2.RetentionPolicy{ + OnDelete: apiv2.DetachOnDelete, + }, + MySQL: map[string]apiv2.MySQLSpec{ + apiv2.DefaultInstanceName: { + ManagedMysql: &apiv2.ManagedMysqlSpec{}, + }, + }, + Redis: map[string]apiv2.RedisSpec{ + apiv2.DefaultInstanceName: { + ManagedRedis: &apiv2.ManagedRedisSpec{}, + }, + }, + Kafka: apiv2.KafkaSpec{ + ManagedKafka: &apiv2.ManagedKafkaSpec{}, + }, + ObjectStore: map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: { + ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{ + Namespace: infraNamespace, + }, + }, + }, + ClickHouse: map[string]apiv2.ClickHouseSpec{ + apiv2.DefaultInstanceName: { + ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}, + }, + }, + }, + } + + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-seaweedfs-s3", name), + Namespace: infraNamespace, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{ + Name: "s3-http", + Port: 80, + }}, + }, + } + + return wandb, service +} + +func markWandbReadyForNetworking(ctx context.Context, name, namespace string) *apiv2.WeightsAndBiases { + wandb := getWandb(ctx, name, namespace) + wandb.Status.MySQLStatus = map[string]apiv2.MysqlInfraStatus{ + apiv2.DefaultInstanceName: { + WBInfraStatus: apiv2.WBInfraStatus{Ready: true}, + Connection: apiv2.MysqlConnection{ + URL: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: "mysql-url", + }, + }, + }, + } + wandb.Status.RedisStatus = map[string]apiv2.RedisInfraStatus{ + apiv2.DefaultInstanceName: { + WBInfraStatus: apiv2.WBInfraStatus{Ready: true}, + }, + } + wandb.Status.KafkaStatus.Ready = true + wandb.Status.ObjectStoreStatus = map[string]apiv2.ObjectStoreInfraStatus{ + apiv2.DefaultInstanceName: { + WBInfraStatus: apiv2.WBInfraStatus{Ready: true}, + }, + } + wandb.Status.ClickHouseStatus = map[string]apiv2.ClickHouseInfraStatus{ + apiv2.DefaultInstanceName: { + WBInfraStatus: apiv2.WBInfraStatus{Ready: true}, + Connection: apiv2.ClickHouseConnection{ + URL: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: "clickhouse-url", + }, + }, + }, + } + wandb.Status.Wandb.Migration.Version = wandb.Spec.Wandb.Version + wandb.Status.Wandb.Migration.LastSuccessVersion = wandb.Spec.Wandb.Version + wandb.Status.Wandb.Migration.Ready = true + wandb.Status.Wandb.Migration.Reason = "Complete" + wandb.Status.Wandb.MySQLInit = map[string]apiv2.MigrationJobStatus{ + apiv2.DefaultInstanceName: {Succeeded: true}, + } + Expect(k8sClient.Status().Update(ctx, wandb)).To(Succeed()) + + return getWandb(ctx, name, namespace) +} + +func reconcileNetworkingManifest(ctx context.Context, wandb *apiv2.WeightsAndBiases) { + wandbManifest, err := manifest.GetServerManifest(ctx, wandb.Spec.Wandb.ManifestRepository, wandb.Spec.Wandb.Version) + Expect(err).NotTo(HaveOccurred()) + + _, err = v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).NotTo(HaveOccurred()) +} + +func getWandb(ctx context.Context, name, namespace string) *apiv2.WeightsAndBiases { + wandb := &apiv2.WeightsAndBiases{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, wandb)).To(Succeed()) + return wandb +} + +func createNamespaceIfMissing(ctx context.Context, name string) { + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}} + err := k8sClient.Create(ctx, ns) + if err != nil && !apierrors.IsAlreadyExists(err) { + Expect(err).NotTo(HaveOccurred()) + } +} + +func deleteIfPresent(ctx context.Context, obj client.Object) { + _ = k8sClient.Delete(ctx, obj) +} diff --git a/internal/controller/weightsandbiases_controller_test.go b/internal/controller/weightsandbiases_controller_test.go index ce38b6a0..93c2a457 100644 --- a/internal/controller/weightsandbiases_controller_test.go +++ b/internal/controller/weightsandbiases_controller_test.go @@ -2,322 +2,742 @@ package controller import ( "context" + "fmt" + "path/filepath" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - wandbcomv1 "github.com/wandb/operator/api/v1" - "github.com/wandb/operator/pkg/wandb/spec" - "github.com/wandb/operator/pkg/wandb/spec/channel/deployer/deployerfakes" - "github.com/wandb/operator/pkg/wandb/spec/charts" - "github.com/wandb/operator/pkg/wandb/spec/state" - "github.com/wandb/operator/pkg/wandb/spec/state/secrets" - + apiv2 "github.com/wandb/operator/api/v2" + v2 "github.com/wandb/operator/internal/controller/reconciler" + "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/pkg/wandb/manifest" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ) -var deployerSpec = spec.Spec{ - Metadata: &spec.Metadata{ - "channelId": "b56e1972-3c78-4de0-af90-e3597bb0785a", - "channelName": "Stable", - "releaseId": "74a7f750-de86-43fe-a945-5350646cf415", - "releaseName": "v202407-2.11", - "releaseCreatedAt": "2024-07-02T18:35:42.479Z", - }, - Chart: &charts.RepoRelease{ - URL: "https://charts.wandb.ai", - Name: "operator-wandb", - Version: "0.14.3", - Debug: false, - }, - Values: spec.Values{ - "app": map[string]interface{}{ - "image": map[string]interface{}{ - "tag": "0.56.0", - "repository": "wandb/local", - }, - }, - "weave": map[string]interface{}{ - "image": map[string]interface{}{ - "tag": "0.56.0", - "repository": "wandb/local", - }, - }, - "console": map[string]interface{}{ - "image": map[string]interface{}{ - "tag": "2.6.0", - "repository": "wandb/console", +var manifestsDir, _ = filepath.Abs("../../hack/testing-manifests/server-manifest") +var manifestsRepository = fmt.Sprintf("file://%s", manifestsDir) + +var _ = Describe("WeightsAndBiases Controller V2", func() { + const ( + WandbName = "test-wandb-v2" + WandbNamespace = "default" + timeout = time.Second * 10 + interval = time.Millisecond * 250 + ) + + AfterEach(func() { + // Cleanup + wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: WandbName, Namespace: WandbNamespace}} + wandbLookupKey := types.NamespacedName{Name: wandb.Name, Namespace: wandb.Namespace} + dbSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: WandbName + "-db-password", + Namespace: WandbNamespace, }, - }, - "parquet": map[string]interface{}{ - "image": map[string]interface{}{ - "tag": "0.56.0", - "repository": "wandb/local", + } + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: WandbName + "-mysql-init", + Namespace: WandbNamespace, }, - }, - }, -} - -var recorder *record.FakeRecorder -var reconciler *WeightsAndBiasesReconciler + } + // Applications are not owned by the WeightsAndBiases CR via OwnerReference + // in the envtest setup, so garbage collection doesn't cascade. Without + // this explicit pass, Applications from earlier It blocks leak into the + // next test's namespace and break assertions that count them. + appList := &apiv2.ApplicationList{} + if err := k8sClient.List(ctx, appList, client.InNamespace(WandbNamespace)); err == nil { + for i := range appList.Items { + app := &appList.Items[i] + if len(app.Finalizers) > 0 { + app.SetFinalizers(nil) + _ = k8sClient.Update(ctx, app) + } + _ = k8sClient.Delete(ctx, app, client.PropagationPolicy(metav1.DeletePropagationBackground)) + } + } + Expect(k8sClient.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))).Should(SatisfyAny(Succeed(), MatchError(ContainSubstring("not found")))) + Expect(k8sClient.Delete(ctx, dbSecret)).Should(SatisfyAny(Succeed(), MatchError(ContainSubstring("not found")))) + Expect(k8sClient.Delete(ctx, wandb)).Should(SatisfyAny(Succeed(), MatchError(ContainSubstring("not found")))) + err := k8sClient.Get(ctx, wandbLookupKey, wandb) + if !errors.IsNotFound(err) { + wandb.SetFinalizers([]string{}) + Expect(k8sClient.Update(ctx, wandb)).Should(Succeed()) + } + }) -var _ = Describe("WeightsandbiasesController", func() { - Describe("DryRun Reconcile", func() { - BeforeEach(func() { + Context("When reconciling a v2 WeightsAndBiases object", func() { + It("Should successfully reconcile and update status", func() { + By("Creating a new WeightsAndBiases v2 object") ctx := context.Background() - recorder = record.NewFakeRecorder(10) - deployerClient := &deployerfakes.FakeDeployerInterface{} - deployerClient.GetSpecReturns(&deployerSpec, nil) - reconciler = &WeightsAndBiasesReconciler{ - Client: k8sClient, - IsAirgapped: false, - DeployerClient: deployerClient, - Scheme: scheme.Scheme, - Recorder: recorder, - DryRun: true, - } - wandb := wandbcomv1.WeightsAndBiases{ + wandb := &apiv2.WeightsAndBiases{ ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "default", + Name: WandbName, + Namespace: WandbNamespace, }, - Spec: wandbcomv1.WeightsAndBiasesSpec{ - Chart: wandbcomv1.Object{Object: map[string]interface{}{}}, - Values: wandbcomv1.Object{Object: map[string]interface{}{ - "global": map[string]interface{}{ - "host": "https://qa-google.wandb.io", + Spec: apiv2.WeightsAndBiasesSpec{ + Size: apiv2.SizeDev, + Wandb: apiv2.WandbAppSpec{ + Hostname: "http://localhost", + Features: map[string]bool{ + "proxy": true, + }, + ManifestRepository: manifestsRepository, + Version: "0.83.0-clickhouse-keeper.2", + }, + MySQL: map[string]apiv2.MySQLSpec{ + apiv2.DefaultInstanceName: { + ManagedMysql: &apiv2.ManagedMysqlSpec{ + StorageSize: "10Gi", + }, + }, + }, + Redis: map[string]apiv2.RedisSpec{ + apiv2.DefaultInstanceName: { + ManagedRedis: &apiv2.ManagedRedisSpec{ + StorageSize: "10Gi", + }, + }, + }, + Kafka: apiv2.KafkaSpec{ + ManagedKafka: &apiv2.ManagedKafkaSpec{ + StorageSize: "10Gi", }, - "ingress": map[string]interface{}{ - "annotations": map[string]interface{}{ - "ingress.gcp.kubernetes.io/pre-shared-cert": "wandb-qa-local-cert-content-hawk", - "kubernetes.io/ingress.class": "gce", - "kubernetes.io/ingress.global-static-ip-name": "wandb-qa-local-operator-address", + }, + ObjectStore: map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: { + ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{ + StorageSize: "10Gi", }, - "nameOverride": "wandb-qa-local", }, - }}, + }, + ClickHouse: map[string]apiv2.ClickHouseSpec{ + apiv2.DefaultInstanceName: { + ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}, + }, + }, }, + Status: apiv2.WeightsAndBiasesStatus{}, } - err := k8sClient.Create(ctx, &wandb) - Expect(err).ToNot(HaveOccurred()) - res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Name: wandb.Name, Namespace: wandb.Namespace}}) - Expect(err).ToNot(HaveOccurred()) - Expect(res).To(Equal(ctrl.Result{RequeueAfter: time.Duration(1 * time.Hour)})) + Expect(k8sClient.Create(ctx, wandb)).Should(Succeed()) + + wandbLookupKey := types.NamespacedName{Name: WandbName, Namespace: WandbNamespace} + + By("Running the reconciler") + reconciler := &WeightsAndBiasesReconciler{ + Client: k8sClient, + Scheme: scheme.Scheme, + Recorder: record.NewFakeRecorder(10), + EnableV2: true, + } + + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + + _, err := reconciler.Reconcile(ctx, ctrl.Request{ + NamespacedName: wandbLookupKey, + }) + + Expect(err).Should(Succeed()) + + By("Checking if finalizers were added") + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + + Expect(utils.ContainsString(wandb.GetFinalizers(), "wandb.apps.wandb.com/cleanup")).Should(BeTrue()) }) - AfterEach(func() { + + It("Should create a MySQL init job when deployment type is moco", func() { + By("Creating a new WeightsAndBiases v2 object with MySQL deployment type 'moco'") ctx := context.Background() - wandb := wandbcomv1.WeightsAndBiases{} - err := k8sClient.Get(ctx, types.NamespacedName{Name: "test", Namespace: "default"}, &wandb) - Expect(err).ToNot(HaveOccurred()) - err = k8sClient.Delete(ctx, &wandb) - Expect(err).ToNot(HaveOccurred()) - err = k8sClient.Delete(ctx, &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "test-spec-active", Namespace: "default"}}) - Expect(err).ToNot(HaveOccurred()) - _, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Name: wandb.Name, Namespace: wandb.Namespace}}) - Expect(err).ToNot(HaveOccurred()) - err = k8sClient.Get(ctx, types.NamespacedName{Name: "test", Namespace: "default"}, &wandb) - Expect(err).To(HaveOccurred()) - }) - Context("When a WeightsAndBiases instance is created", func() { - It("Should add the finalizer to the instance", func() { - ctx := context.Background() - wandb := wandbcomv1.WeightsAndBiases{} - err := k8sClient.Get(ctx, types.NamespacedName{Name: "test", Namespace: "default"}, &wandb) - Expect(err).ToNot(HaveOccurred()) - Expect(wandb.ObjectMeta.Finalizers).To(ContainElement(resFinalizer)) - }) - It("Should record a sequence of events", func() { - Expect(recorder.Events).To(HaveLen(3)) - event := <-recorder.Events - Expect(event).To(ContainSubstring("Normal Reconciling Reconciling")) - event = <-recorder.Events - Expect(event).To(ContainSubstring("Normal LoadingConfig Loading desired configuration")) - event = <-recorder.Events - Expect(event).To(ContainSubstring("Completed reconcile successfully")) + wandbName := "test-moco-init" + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{ + Name: wandbName, + Namespace: WandbNamespace, + }, + Spec: apiv2.WeightsAndBiasesSpec{ + Wandb: apiv2.WandbAppSpec{ + Hostname: "http://localhost", + Features: map[string]bool{}, + ManifestRepository: manifestsRepository, + Version: "0.83.0-clickhouse-keeper.2", + }, + MySQL: map[string]apiv2.MySQLSpec{ + apiv2.DefaultInstanceName: {ManagedMysql: &apiv2.ManagedMysqlSpec{}}, + }, + Redis: map[string]apiv2.RedisSpec{apiv2.DefaultInstanceName: {ManagedRedis: &apiv2.ManagedRedisSpec{}}}, + Kafka: apiv2.KafkaSpec{ManagedKafka: &apiv2.ManagedKafkaSpec{}}, + ObjectStore: map[string]apiv2.ObjectStoreSpec{apiv2.DefaultInstanceName: {ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{}}}, + ClickHouse: map[string]apiv2.ClickHouseSpec{apiv2.DefaultInstanceName: {ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}}}, + }, + } + Expect(k8sClient.Create(ctx, wandb)).Should(Succeed()) + + By("Setting infra to ready") + wandb.Status.MySQLStatus = map[string]apiv2.MysqlInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.RedisStatus = map[string]apiv2.RedisInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.KafkaStatus.Ready = true + wandb.Status.ObjectStoreStatus = map[string]apiv2.ObjectStoreInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.ClickHouseStatus = map[string]apiv2.ClickHouseInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + Expect(k8sClient.Status().Update(ctx, wandb)).Should(Succeed()) + + By("Creating the db-password secret") + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: wandbName + "-db-password", + Namespace: WandbNamespace, + }, + Data: map[string][]byte{ + "rootPassword": []byte("root-pass"), + "password": []byte("user-pass"), + }, + } + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + + wandbLookupKey := types.NamespacedName{Name: wandb.Name, Namespace: wandb.Namespace} + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + + By("Running the reconciler") + reconciler := &WeightsAndBiasesReconciler{ + Client: k8sClient, + Scheme: scheme.Scheme, + Recorder: record.NewFakeRecorder(10), + EnableV2: true, + } + + _, err := reconciler.Reconcile(ctx, ctrl.Request{ + NamespacedName: wandbLookupKey, }) - It("Should create an empty User Spec", func() { - ctx := context.Background() - wandb := wandbcomv1.WeightsAndBiases{} - err := k8sClient.Get(ctx, types.NamespacedName{Name: "test", Namespace: "default"}, &wandb) - Expect(err).ToNot(HaveOccurred()) - specManager := state.New(ctx, k8sClient, &wandb, scheme.Scheme, secrets.New(ctx, k8sClient, &wandb, scheme.Scheme)) - userSpec, err := specManager.GetUserInput() - Expect(err).ToNot(HaveOccurred()) - Expect(userSpec.Values).To(BeEmpty()) - Expect(userSpec.Chart).To(BeNil()) - Expect(userSpec.Metadata).To(BeNil()) + Expect(err).Should(Succeed()) + + By("Setting infrastructure status to ready") + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + + wandb.Status.MySQLStatus = map[string]apiv2.MysqlInfraStatus{apiv2.DefaultInstanceName: { + WBInfraStatus: apiv2.WBInfraStatus{Ready: true}, + Connection: apiv2.MysqlConnection{URL: v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: WandbName}, Key: "test"}}, + }} + wandb.Status.RedisStatus = map[string]apiv2.RedisInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.KafkaStatus.Ready = true + wandb.Status.ObjectStoreStatus = map[string]apiv2.ObjectStoreInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.ClickHouseStatus = map[string]apiv2.ClickHouseInfraStatus{apiv2.DefaultInstanceName: { + WBInfraStatus: apiv2.WBInfraStatus{Ready: true}, + Connection: apiv2.ClickHouseConnection{URL: v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: WandbName}, Key: "test"}}, + }} + + Expect(k8sClient.Status().Update(ctx, wandb)).Should(Succeed()) + + By("Checking if Applications were NOT created yet (migrations not complete)") + wandbManifest, err := manifest.GetServerManifest(ctx, wandb.Spec.Wandb.ManifestRepository, wandb.Spec.Wandb.Version) + Expect(err).Should(Succeed()) + _, err = v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).Should(Succeed()) + + By("Checking if the MySQL init job was created") + // The init job is now per managed MySQL instance, named after the + // instance's resource name (default instance: "<cr>-mysql"). + job := &batchv1.Job{} + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: wandbName + "-mysql-moco-init", Namespace: WandbNamespace}, job) + }, timeout, interval).Should(Succeed()) + + Expect(job.Spec.Template.Spec.Containers[0].Name).To(Equal("moco-init")) + }) + + It("Should create application components when infrastructure is ready", func() { + By("Creating a new WeightsAndBiases v2 object with ready infrastructure") + ctx := context.Background() + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{ + Name: WandbName, + Namespace: WandbNamespace, + }, + Spec: apiv2.WeightsAndBiasesSpec{ + Size: apiv2.SizeDev, + Wandb: apiv2.WandbAppSpec{ + Hostname: "http://localhost", + Features: map[string]bool{}, + ManifestRepository: manifestsRepository, + Version: "0.83.0-clickhouse-keeper.2", + }, + MySQL: map[string]apiv2.MySQLSpec{ + apiv2.DefaultInstanceName: {ManagedMysql: &apiv2.ManagedMysqlSpec{}}, + }, + Redis: map[string]apiv2.RedisSpec{ + apiv2.DefaultInstanceName: {ManagedRedis: &apiv2.ManagedRedisSpec{}}, + }, + Kafka: apiv2.KafkaSpec{ + ManagedKafka: &apiv2.ManagedKafkaSpec{}, + }, + ObjectStore: map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: {ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{}}, + }, + ClickHouse: map[string]apiv2.ClickHouseSpec{ + apiv2.DefaultInstanceName: {ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}}, + }, + }, + } + Expect(k8sClient.Create(ctx, wandb)).Should(Succeed()) + + wandbLookupKey := types.NamespacedName{Name: wandb.Name, Namespace: wandb.Namespace} + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + + By("Running the reconciler") + reconciler := &WeightsAndBiasesReconciler{ + Client: k8sClient, + Scheme: scheme.Scheme, + Recorder: record.NewFakeRecorder(10), + EnableV2: true, + } + + _, err := reconciler.Reconcile(ctx, ctrl.Request{ + NamespacedName: wandbLookupKey, }) + Expect(err).Should(Succeed()) + + By("Setting infrastructure status to ready") + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + + wandb.Status.MySQLStatus = map[string]apiv2.MysqlInfraStatus{apiv2.DefaultInstanceName: { + WBInfraStatus: apiv2.WBInfraStatus{Ready: true}, + Connection: apiv2.MysqlConnection{URL: v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: WandbName}, Key: "test"}}, + }} + wandb.Status.RedisStatus = map[string]apiv2.RedisInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.KafkaStatus.Ready = true + wandb.Status.ObjectStoreStatus = map[string]apiv2.ObjectStoreInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.ClickHouseStatus = map[string]apiv2.ClickHouseInfraStatus{apiv2.DefaultInstanceName: { + WBInfraStatus: apiv2.WBInfraStatus{Ready: true}, + Connection: apiv2.ClickHouseConnection{URL: v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: WandbName}, Key: "test"}}, + }} + + Expect(k8sClient.Status().Update(ctx, wandb)).Should(Succeed()) + + By("Checking if Applications were NOT created yet (migrations not complete)") + wandbManifest, err := manifest.GetServerManifest(ctx, wandb.Spec.Wandb.ManifestRepository, wandb.Spec.Wandb.Version) + Expect(err).Should(Succeed()) + ctrlResult, err := v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).Should(Succeed()) + Expect(ctrlResult.RequeueAfter).Should(BeNumerically(">", 0)) + + appList := &apiv2.ApplicationList{} + Expect(k8sClient.List(ctx, appList, client.InNamespace(WandbNamespace))).Should(Succeed()) + Expect(len(appList.Items)).Should(Equal(0)) + + By("Setting migration status to successful") + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + wandb.Status.Wandb.Migration.Version = wandb.Spec.Wandb.Version + wandb.Status.Wandb.Migration.LastSuccessVersion = wandb.Spec.Wandb.Version + wandb.Status.Wandb.Migration.Ready = true + wandb.Status.Wandb.Migration.Reason = "Complete" + wandb.Status.Wandb.MySQLInit = map[string]apiv2.MigrationJobStatus{apiv2.DefaultInstanceName: {Succeeded: true}} + Expect(k8sClient.Status().Update(ctx, wandb)).Should(Succeed()) + + // For now test by calling ReconcileWandbManifest directly, but this will get refactored into the reconciler later + ctrlResult, err = v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).Should(Succeed()) + Expect(ctrlResult.RequeueAfter).Should(BeZero()) + + By("Checking if ServiceAccount was created") + saLookupKey := types.NamespacedName{Name: wandb.Spec.Wandb.ServiceAccount.ServiceAccountName, Namespace: WandbNamespace} + createdSa := &v1.ServiceAccount{} + Expect(k8sClient.Get(ctx, saLookupKey, createdSa)).Should(Succeed()) + Expect(createdSa.Labels["app.kubernetes.io/instance"]).To(Equal(WandbName)) + + By("Checking if Applications were created") + appList = &apiv2.ApplicationList{} + Expect(k8sClient.List(ctx, appList, client.InNamespace(WandbNamespace))).Should(Succeed()) + + // The 0.76.1.yaml manifest should have some applications defined. + // We expect them to be created as Application CRs. + Expect(len(appList.Items)).Should(BeNumerically("==", len(wandbManifest.Applications)-1), "Expected all non-feature flagged applications to be created") }) - }) - Describe("Reconcile with _releaseId set", func() { - BeforeEach(func() { + + It("Should advance status.observedGeneration only once applications are reconciled for a generation", func() { + By("Creating a new WeightsAndBiases v2 object at the initial version") ctx := context.Background() - recorder = record.NewFakeRecorder(10) - deployerClient := &deployerfakes.FakeDeployerInterface{} - deployerClient.GetSpecReturns(&deployerSpec, nil) - reconciler = &WeightsAndBiasesReconciler{ - Client: k8sClient, - IsAirgapped: false, - DeployerClient: deployerClient, - Scheme: scheme.Scheme, - Recorder: recorder, - DryRun: true, + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{ + Name: WandbName, + Namespace: WandbNamespace, + }, + Spec: apiv2.WeightsAndBiasesSpec{ + Size: apiv2.SizeDev, + Wandb: apiv2.WandbAppSpec{ + Hostname: "http://localhost", + Features: map[string]bool{}, + ManifestRepository: manifestsRepository, + Version: "0.83.0-clickhouse-keeper.1", + }, + MySQL: map[string]apiv2.MySQLSpec{apiv2.DefaultInstanceName: {ManagedMysql: &apiv2.ManagedMysqlSpec{}}}, + Redis: map[string]apiv2.RedisSpec{apiv2.DefaultInstanceName: {ManagedRedis: &apiv2.ManagedRedisSpec{}}}, + Kafka: apiv2.KafkaSpec{ManagedKafka: &apiv2.ManagedKafkaSpec{}}, + ObjectStore: map[string]apiv2.ObjectStoreSpec{apiv2.DefaultInstanceName: {ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{}}}, + ClickHouse: map[string]apiv2.ClickHouseSpec{apiv2.DefaultInstanceName: {ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}}}, + }, } - wandb := wandbcomv1.WeightsAndBiases{ + Expect(k8sClient.Create(ctx, wandb)).Should(Succeed()) + + wandbLookupKey := types.NamespacedName{Name: wandb.Name, Namespace: wandb.Namespace} + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + Expect(wandb.Status.ObservedGeneration).Should(BeZero()) + + By("Marking infrastructure, mysql init, and migrations ready") + wandb.Status.MySQLStatus = map[string]apiv2.MysqlInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.RedisStatus = map[string]apiv2.RedisInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.KafkaStatus.Ready = true + wandb.Status.ObjectStoreStatus = map[string]apiv2.ObjectStoreInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.ClickHouseStatus = map[string]apiv2.ClickHouseInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + mysqlStatus := wandb.Status.MySQLStatus[apiv2.DefaultInstanceName] + mysqlStatus.Connection.URL = v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: WandbName}, Key: "test"} + wandb.Status.MySQLStatus[apiv2.DefaultInstanceName] = mysqlStatus + clickHouseStatus := wandb.Status.ClickHouseStatus[apiv2.DefaultInstanceName] + clickHouseStatus.Connection.URL = v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: WandbName}, Key: "test"} + wandb.Status.ClickHouseStatus[apiv2.DefaultInstanceName] = clickHouseStatus + wandb.Status.Wandb.Migration.Version = wandb.Spec.Wandb.Version + wandb.Status.Wandb.Migration.LastSuccessVersion = wandb.Spec.Wandb.Version + wandb.Status.Wandb.Migration.Ready = true + wandb.Status.Wandb.Migration.Reason = "Complete" + wandb.Status.Wandb.MySQLInit = map[string]apiv2.MigrationJobStatus{apiv2.DefaultInstanceName: {Succeeded: true}} + Expect(k8sClient.Status().Update(ctx, wandb)).Should(Succeed()) + + By("Reconciling the manifest to completion for the initial generation") + wandbManifest, err := manifest.GetServerManifest(ctx, wandb.Spec.Wandb.ManifestRepository, wandb.Spec.Wandb.Version) + Expect(err).Should(Succeed()) + ctrlResult, err := v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).Should(Succeed()) + Expect(ctrlResult.RequeueAfter).Should(BeZero()) + + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + initialGeneration := wandb.Generation + Expect(initialGeneration).Should(BeNumerically(">", 0)) + Expect(wandb.Status.ObservedGeneration).Should(Equal(initialGeneration)) + + By("Upgrading spec.wandb.version to bump the generation") + wandb.Spec.Wandb.Version = "0.83.0-clickhouse-keeper.2" + Expect(k8sClient.Update(ctx, wandb)).Should(Succeed()) + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + Expect(wandb.Generation).Should(BeNumerically(">", initialGeneration)) + Expect(wandb.Status.ObservedGeneration).Should(Equal(initialGeneration)) + + By("Reconciling while the new version's migration is still pending") + wandbManifest, err = manifest.GetServerManifest(ctx, wandb.Spec.Wandb.ManifestRepository, wandb.Spec.Wandb.Version) + Expect(err).Should(Succeed()) + ctrlResult, err = v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).Should(Succeed()) + Expect(ctrlResult.RequeueAfter).Should(BeNumerically(">", 0)) + + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + Expect(wandb.Status.ObservedGeneration).Should(Equal(initialGeneration), + "observedGeneration must not advance before applications carry the new generation's spec") + + By("Completing the migration and reconciling to completion") + wandb.Status.Wandb.Migration.Version = wandb.Spec.Wandb.Version + wandb.Status.Wandb.Migration.LastSuccessVersion = wandb.Spec.Wandb.Version + wandb.Status.Wandb.Migration.Ready = true + wandb.Status.Wandb.Migration.Reason = "Complete" + Expect(k8sClient.Status().Update(ctx, wandb)).Should(Succeed()) + + ctrlResult, err = v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).Should(Succeed()) + Expect(ctrlResult.RequeueAfter).Should(BeZero()) + + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + Expect(wandb.Status.ObservedGeneration).Should(Equal(wandb.Generation)) + Expect(wandb.Status.ObservedGeneration).Should(BeNumerically(">", initialGeneration)) + }) + + It("Should clean up legacy v1 deployments once live Deployments are ready, even with a stale status map", func() { + By("Creating a new WeightsAndBiases v2 object") + ctx := context.Background() + wandb := &apiv2.WeightsAndBiases{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-release-id", - Namespace: "default", + Name: WandbName, + Namespace: WandbNamespace, }, - Spec: wandbcomv1.WeightsAndBiasesSpec{ - Chart: wandbcomv1.Object{Object: map[string]interface{}{}}, - Values: wandbcomv1.Object{Object: map[string]interface{}{ - "global": map[string]interface{}{ - "host": "https://qa-google.wandb.io", - }, - }}, + Spec: apiv2.WeightsAndBiasesSpec{ + Size: apiv2.SizeDev, + Wandb: apiv2.WandbAppSpec{ + Hostname: "http://localhost", + Features: map[string]bool{}, + ManifestRepository: manifestsRepository, + Version: "0.83.0-clickhouse-keeper.1", + }, + MySQL: map[string]apiv2.MySQLSpec{apiv2.DefaultInstanceName: {ManagedMysql: &apiv2.ManagedMysqlSpec{}}}, + Redis: map[string]apiv2.RedisSpec{apiv2.DefaultInstanceName: {ManagedRedis: &apiv2.ManagedRedisSpec{}}}, + Kafka: apiv2.KafkaSpec{ManagedKafka: &apiv2.ManagedKafkaSpec{}}, + ObjectStore: map[string]apiv2.ObjectStoreSpec{apiv2.DefaultInstanceName: {ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{}}}, + ClickHouse: map[string]apiv2.ClickHouseSpec{apiv2.DefaultInstanceName: {ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}}}, }, } - err := k8sClient.Create(ctx, &wandb) - Expect(err).ToNot(HaveOccurred()) + Expect(k8sClient.Create(ctx, wandb)).Should(Succeed()) + wandbLookupKey := types.NamespacedName{Name: wandb.Name, Namespace: wandb.Namespace} - // Create UserSpec with _releaseId - userSpec := &spec.Spec{ - Values: map[string]interface{}{ - "_releaseId": "0b901113-8135-48ae-bdaf-6fa82b4b2d28", + By("Creating a legacy v1 helm Deployment left over from the upgrade") + legacy := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: WandbName + "-app-bc", Namespace: WandbNamespace}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "legacy"}}, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "legacy"}}, + Spec: v1.PodSpec{Containers: []v1.Container{{Name: "app", Image: "wandb/local:latest"}}}, + }, }, } - err = state.New(ctx, k8sClient, &wandb, scheme.Scheme, secrets.New(ctx, k8sClient, &wandb, scheme.Scheme)).SetUserInput(userSpec) - Expect(err).ToNot(HaveOccurred()) + Expect(k8sClient.Create(ctx, legacy)).Should(Succeed()) + DeferCleanup(func() { + _ = k8sClient.Delete(ctx, legacy) + }) + + By("Marking infrastructure, mysql init, and migrations ready") + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + wandb.Status.MySQLStatus = map[string]apiv2.MysqlInfraStatus{apiv2.DefaultInstanceName: { + WBInfraStatus: apiv2.WBInfraStatus{Ready: true}, + Connection: apiv2.MysqlConnection{URL: v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: WandbName}, Key: "test"}}, + }} + wandb.Status.RedisStatus = map[string]apiv2.RedisInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.KafkaStatus.Ready = true + wandb.Status.ObjectStoreStatus = map[string]apiv2.ObjectStoreInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.ClickHouseStatus = map[string]apiv2.ClickHouseInfraStatus{apiv2.DefaultInstanceName: { + WBInfraStatus: apiv2.WBInfraStatus{Ready: true}, + Connection: apiv2.ClickHouseConnection{URL: v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: WandbName}, Key: "test"}}, + }} + wandb.Status.Wandb.Migration.Version = wandb.Spec.Wandb.Version + wandb.Status.Wandb.Migration.LastSuccessVersion = wandb.Spec.Wandb.Version + wandb.Status.Wandb.Migration.Ready = true + wandb.Status.Wandb.Migration.Reason = "Complete" + wandb.Status.Wandb.MySQLInit = map[string]apiv2.MigrationJobStatus{apiv2.DefaultInstanceName: {Succeeded: true}} + Expect(k8sClient.Status().Update(ctx, wandb)).Should(Succeed()) + + By("Reconciling the manifest to create the Applications") + wandbManifest, err := manifest.GetServerManifest(ctx, wandb.Spec.Wandb.ManifestRepository, wandb.Spec.Wandb.Version) + Expect(err).Should(Succeed()) + _, err = v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).Should(Succeed()) + + appList := &apiv2.ApplicationList{} + Expect(k8sClient.List(ctx, appList, client.InNamespace(WandbNamespace))).Should(Succeed()) + Expect(appList.Items).ShouldNot(BeEmpty()) + + By("Verifying Applications carry a WeightsAndBiases owner reference for the MatchEveryOwner watch") + var ownerKinds []string + for _, ref := range appList.Items[0].OwnerReferences { + ownerKinds = append(ownerKinds, ref.Kind) + } + Expect(ownerKinds).To(ContainElement("WeightsAndBiases"), + "the parent's Owns(Application, MatchEveryOwner) watch maps events through this owner ref") - res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Name: wandb.Name, Namespace: wandb.Namespace}}) - Expect(err).ToNot(HaveOccurred()) - Expect(res).To(Equal(ctrl.Result{RequeueAfter: time.Duration(1 * time.Hour)})) + By("Verifying cleanup is deferred while application Deployments are absent") + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: legacy.Name, Namespace: WandbNamespace}, &appsv1.Deployment{})).Should(Succeed(), + "legacy deployment must survive until the estate is ready") + + By("Simulating the Application controller: rolled-out Deployments while the status map stays stale-false") + for i := range appList.Items { + app := appList.Items[i] + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: app.Name, Namespace: WandbNamespace}, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": app.Name}}, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": app.Name}}, + Spec: v1.PodSpec{Containers: []v1.Container{{Name: "app", Image: "wandb/local:latest"}}}, + }, + }, + } + Expect(k8sClient.Create(ctx, dep)).Should(Succeed()) + DeferCleanup(func() { + _ = k8sClient.Delete(ctx, dep) + }) + dep.Status = appsv1.DeploymentStatus{ + ObservedGeneration: dep.Generation, + Replicas: 1, + ReadyReplicas: 1, + } + Expect(k8sClient.Status().Update(ctx, dep)).Should(Succeed()) + } + + By("Reconciling again: the gate must pass on live Deployments even though the status map says not-ready") + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + _, err = v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).Should(Succeed()) + + err = k8sClient.Get(ctx, types.NamespacedName{Name: legacy.Name, Namespace: WandbNamespace}, &appsv1.Deployment{}) + Expect(errors.IsNotFound(err)).To(BeTrue(), + "legacy -bc deployment must be deleted once live Deployments are ready") + + By("Verifying the status map refreshes from live Application status on the next pass") + refreshed := &apiv2.Application{} + appName := appList.Items[0].Name + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: appName, Namespace: WandbNamespace}, refreshed)).Should(Succeed()) + refreshed.Status.Ready = true + Expect(k8sClient.Status().Update(ctx, refreshed)).Should(Succeed()) + + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + _, err = v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).Should(Succeed()) + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + Expect(wandb.Status.Wandb.Applications[appName].Ready).To(BeTrue(), + "the parent status map must reflect the Application's current status") }) - AfterEach(func() { + It("Should handle various migration states correctly", func() { + By("Creating a new WeightsAndBiases v2 object") ctx := context.Background() - wandb := wandbcomv1.WeightsAndBiases{} - err := k8sClient.Get(ctx, types.NamespacedName{Name: "test-release-id", Namespace: "default"}, &wandb) - Expect(err).ToNot(HaveOccurred()) - err = k8sClient.Delete(ctx, &wandb) - Expect(err).ToNot(HaveOccurred()) - err = k8sClient.Delete(ctx, &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "test-release-id-spec-active", Namespace: "default"}}) - Expect(err).ToNot(HaveOccurred()) - _, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Name: wandb.Name, Namespace: wandb.Namespace}}) - Expect(err).ToNot(HaveOccurred()) - err = k8sClient.Get(ctx, types.NamespacedName{Name: "test-release-id", Namespace: "default"}, &wandb) - Expect(err).To(HaveOccurred()) + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{ + Name: WandbName, + Namespace: WandbNamespace, + }, + Spec: apiv2.WeightsAndBiasesSpec{ + Size: apiv2.SizeDev, + Wandb: apiv2.WandbAppSpec{ + Hostname: "http://localhost", + Features: map[string]bool{}, + ManifestRepository: manifestsRepository, + Version: "0.83.0-clickhouse-keeper.2", + }, + MySQL: map[string]apiv2.MySQLSpec{apiv2.DefaultInstanceName: {ManagedMysql: &apiv2.ManagedMysqlSpec{}}}, + Redis: map[string]apiv2.RedisSpec{apiv2.DefaultInstanceName: {ManagedRedis: &apiv2.ManagedRedisSpec{}}}, + Kafka: apiv2.KafkaSpec{ManagedKafka: &apiv2.ManagedKafkaSpec{}}, + ObjectStore: map[string]apiv2.ObjectStoreSpec{apiv2.DefaultInstanceName: {ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{}}}, + ClickHouse: map[string]apiv2.ClickHouseSpec{apiv2.DefaultInstanceName: {ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}}}, + }, + } + Expect(k8sClient.Create(ctx, wandb)).Should(Succeed()) + + wandbLookupKey := types.NamespacedName{Name: wandb.Name, Namespace: wandb.Namespace} + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + + // Mark infra as ready + wandb.Status.MySQLStatus = map[string]apiv2.MysqlInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.RedisStatus = map[string]apiv2.RedisInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.KafkaStatus.Ready = true + wandb.Status.ObjectStoreStatus = map[string]apiv2.ObjectStoreInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.ClickHouseStatus = map[string]apiv2.ClickHouseInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + Expect(k8sClient.Status().Update(ctx, wandb)).Should(Succeed()) + + wandbManifest, err := manifest.GetServerManifest(ctx, wandb.Spec.Wandb.ManifestRepository, wandb.Spec.Wandb.Version) + Expect(err).Should(Succeed()) + + By("Simulating migration in Running state") + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + wandb.Status.Wandb.Migration.Version = wandb.Spec.Wandb.Version + wandb.Status.Wandb.Migration.Ready = false + wandb.Status.Wandb.Migration.Reason = "Running" + mysqlStatus := wandb.Status.MySQLStatus[apiv2.DefaultInstanceName] + mysqlStatus.Connection.URL = v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: WandbName}, Key: "test"} + wandb.Status.MySQLStatus[apiv2.DefaultInstanceName] = mysqlStatus + clickHouseStatus := wandb.Status.ClickHouseStatus[apiv2.DefaultInstanceName] + clickHouseStatus.Connection.URL = v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: WandbName}, Key: "test"} + wandb.Status.ClickHouseStatus[apiv2.DefaultInstanceName] = clickHouseStatus + Expect(k8sClient.Status().Update(ctx, wandb)).Should(Succeed()) + + ctrlResult, err := v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).Should(Succeed()) + Expect(ctrlResult.RequeueAfter).Should(BeNumerically(">", 0), "Expected requeue when migration is running") + + By("Simulating migration in Failed state") + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + wandb.Status.Wandb.Migration.Ready = false + wandb.Status.Wandb.Migration.Reason = "Failed" + Expect(k8sClient.Status().Update(ctx, wandb)).Should(Succeed()) + + ctrlResult, err = v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).Should(Succeed()) + Expect(ctrlResult.RequeueAfter).Should(BeNumerically(">", 0), "Expected requeue when migration failed") + + By("Simulating migration Complete") + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + wandb.Status.Wandb.Migration.Ready = true + wandb.Status.Wandb.Migration.Reason = "Complete" + wandb.Status.Wandb.Migration.LastSuccessVersion = wandb.Spec.Wandb.Version + wandb.Status.Wandb.MySQLInit = map[string]apiv2.MigrationJobStatus{apiv2.DefaultInstanceName: {Succeeded: true}} + Expect(k8sClient.Status().Update(ctx, wandb)).Should(Succeed()) + + ctrlResult, err = v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).Should(Succeed()) + Expect(ctrlResult.RequeueAfter).Should(BeZero(), "Expected no requeue when migration is complete") }) - It("Should use the specified _releaseId from UserSpec in the final spec", func() { + It("Should trigger new migrations on version upgrade", func() { + By("Creating a new WeightsAndBiases v2 object with an old version") ctx := context.Background() - wandb := wandbcomv1.WeightsAndBiases{} - err := k8sClient.Get(ctx, types.NamespacedName{Name: "test-release-id", Namespace: "default"}, &wandb) - Expect(err).ToNot(HaveOccurred()) - - specManager := state.New(ctx, k8sClient, &wandb, scheme.Scheme, secrets.New(ctx, k8sClient, &wandb, scheme.Scheme)) - activeSpec, err := specManager.GetActive() - Expect(err).ToNot(HaveOccurred()) - Expect(activeSpec.Values["_releaseId"]).To(Equal("0b901113-8135-48ae-bdaf-6fa82b4b2d28")) + oldVersion := "0.83.0-clickhouse-keeper.1" + newVersion := "0.83.0-clickhouse-keeper.2" + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{ + Name: WandbName, + Namespace: WandbNamespace, + }, + Spec: apiv2.WeightsAndBiasesSpec{ + Size: apiv2.SizeDev, + Wandb: apiv2.WandbAppSpec{ + Hostname: "http://localhost", + Features: map[string]bool{}, + ManifestRepository: manifestsRepository, + Version: oldVersion, + }, + MySQL: map[string]apiv2.MySQLSpec{apiv2.DefaultInstanceName: {ManagedMysql: &apiv2.ManagedMysqlSpec{}}}, + Redis: map[string]apiv2.RedisSpec{apiv2.DefaultInstanceName: {ManagedRedis: &apiv2.ManagedRedisSpec{}}}, + Kafka: apiv2.KafkaSpec{ManagedKafka: &apiv2.ManagedKafkaSpec{}}, + ObjectStore: map[string]apiv2.ObjectStoreSpec{apiv2.DefaultInstanceName: {ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{}}}, + ClickHouse: map[string]apiv2.ClickHouseSpec{apiv2.DefaultInstanceName: {ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}}}, + }, + } + Expect(k8sClient.Create(ctx, wandb)).Should(Succeed()) + + wandbLookupKey := types.NamespacedName{Name: wandb.Name, Namespace: wandb.Namespace} + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + + // Mark infra as ready and migration as complete for old version + wandb.Status.MySQLStatus = map[string]apiv2.MysqlInfraStatus{apiv2.DefaultInstanceName: { + WBInfraStatus: apiv2.WBInfraStatus{Ready: true}, + Connection: apiv2.MysqlConnection{URL: v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: WandbName}, Key: "test"}}, + }} + wandb.Status.RedisStatus = map[string]apiv2.RedisInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.KafkaStatus.Ready = true + wandb.Status.ObjectStoreStatus = map[string]apiv2.ObjectStoreInfraStatus{apiv2.DefaultInstanceName: {WBInfraStatus: apiv2.WBInfraStatus{Ready: true}}} + wandb.Status.ClickHouseStatus = map[string]apiv2.ClickHouseInfraStatus{apiv2.DefaultInstanceName: { + WBInfraStatus: apiv2.WBInfraStatus{Ready: true}, + Connection: apiv2.ClickHouseConnection{URL: v1.SecretKeySelector{LocalObjectReference: v1.LocalObjectReference{Name: WandbName}, Key: "test"}}, + }} + wandb.Status.Wandb.Migration.Version = oldVersion + wandb.Status.Wandb.Migration.LastSuccessVersion = oldVersion + wandb.Status.Wandb.Migration.Ready = true + wandb.Status.Wandb.Migration.Reason = "Complete" + wandb.Status.Wandb.MySQLInit = map[string]apiv2.MigrationJobStatus{apiv2.DefaultInstanceName: {Succeeded: true}} + Expect(k8sClient.Status().Update(ctx, wandb)).Should(Succeed()) + + By("Upgrading the version in the spec") + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + wandb.Spec.Wandb.Version = newVersion + Expect(k8sClient.Update(ctx, wandb)).Should(Succeed()) + + By("Running ReconcileWandbManifest and verifying it triggers migrations") + wandbManifest, err := manifest.GetServerManifest(ctx, wandb.Spec.Wandb.ManifestRepository, newVersion) + Expect(err).Should(Succeed()) + + // Re-fetch to get updated Spec and Status + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + + // This call to ReconcileWandbManifest should trigger runMigrations, + // which sees version mismatch and starts migrations. + _, err = v2.ReconcileWandbManifest(ctx, k8sClient, wandb, wandbManifest, v2.DefaultTelemetryRuntimeConfig()) + Expect(err).Should(Succeed()) + + By("Verifying migration status was reset for the new version") + Expect(k8sClient.Get(ctx, wandbLookupKey, wandb)).Should(Succeed()) + Expect(wandb.Status.Wandb.Migration.Version).Should(Equal(newVersion)) + Expect(wandb.Status.Wandb.Migration.Ready).Should(BeFalse()) + Expect(wandb.Status.Wandb.Migration.Reason).Should(Equal("Running")) }) }) - //TODO(dpanzella): Uncomment after fixing the helm kubernetes client to not be the default - //Describe("Reconcile and Apply", func() { - // BeforeEach(func() { - // ctx := context.Background() - // recorder = record.NewFakeRecorder(10) - // deployerClient := &deployerfakes.FakeDeployerInterface{} - // deployerClient.GetSpecReturns(&deployerSpec, nil) - // reconciler = &WeightsAndBiasesReconciler{ - // Client: k8sClient, - // IsAirgapped: false, - // DeployerClient: deployerClient, - // Scheme: scheme.Scheme, - // Recorder: recorder, - // DryRun: false, - // } - // wandb := wandbcomv1.WeightsAndBiases{ - // ObjectMeta: metav1.ObjectMeta{ - // Name: "test", - // Namespace: "default", - // }, - // Spec: wandbcomv1.WeightsAndBiasesSpec{ - // Chart: wandbcomv1.Object{Object: map[string]interface{}{}}, - // Values: wandbcomv1.Object{Object: map[string]interface{}{ - // "global": map[string]interface{}{ - // "host": "https://qa-google.wandb.io", - // }, - // "ingress": map[string]interface{}{ - // "annotations": map[string]interface{}{ - // "ingress.gcp.kubernetes.io/pre-shared-cert": "wandb-qa-local-cert-content-hawk", - // "kubernetes.io/ingress.class": "gce", - // "kubernetes.io/ingress.global-static-ip-name": "wandb-qa-local-operator-address", - // }, - // "nameOverride": "wandb-qa-local", - // }, - // }}, - // }, - // } - // err := k8sClient.Create(ctx, &wandb) - // Expect(err).ToNot(HaveOccurred()) - // res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Name: wandb.Name, Namespace: wandb.Namespace}}) - // Expect(err).ToNot(HaveOccurred()) - // Expect(res).To(Equal(ctrl.Result{RequeueAfter: time.Duration(1 * time.Hour)})) - // }) - // AfterEach(func() { - // ctx := context.Background() - // wandb := wandbcomv1.WeightsAndBiases{} - // err := k8sClient.Get(ctx, types.NamespacedName{Name: "test", Namespace: "default"}, &wandb) - // Expect(err).ToNot(HaveOccurred()) - // err = k8sClient.Delete(ctx, &wandb) - // Expect(err).ToNot(HaveOccurred()) - // err = k8sClient.Delete(ctx, &v1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "test-spec-active", Namespace: "default"}}) - // Expect(err).ToNot(HaveOccurred()) - // err = k8sClient.Get(ctx, types.NamespacedName{Name: "test", Namespace: "default"}, &wandb) - // Expect(err).ToNot(HaveOccurred()) - // controllerutil.RemoveFinalizer(&wandb, resFinalizer) - // err = k8sClient.Update(ctx, &wandb) - // Expect(err).ToNot(HaveOccurred()) - // err = k8sClient.Get(ctx, types.NamespacedName{Name: "test", Namespace: "default"}, &wandb) - // Expect(err).To(HaveOccurred()) - // }) - // Context("When a WeightsAndBiases instance is created", func() { - // It("Should add the finalizer to the instance", func() { - // ctx := context.Background() - // wandb := wandbcomv1.WeightsAndBiases{} - // err := k8sClient.Get(ctx, types.NamespacedName{Name: "test", Namespace: "default"}, &wandb) - // Expect(err).ToNot(HaveOccurred()) - // Expect(wandb.ObjectMeta.Finalizers).To(ContainElement(resFinalizer)) - // }) - // It("Should record a sequence of events", func() { - // Expect(recorder.Events).To(HaveLen(3)) - // event := <-recorder.Events - // Expect(event).To(ContainSubstring("Normal Reconciling Reconciling")) - // event = <-recorder.Events - // Expect(event).To(ContainSubstring("Normal LoadingConfig Loading desired configuration")) - // event = <-recorder.Events - // Expect(event).To(ContainSubstring("Completed reconcile successfully")) - // }) - // It("Should add ownerrefs to the resources", func() { - // ctx := context.Background() - // labels := client.MatchingLabels{ - // "app.kubernetes.io/managed-by": "Helm", - // "app.kubernetes.io/instance": "test", - // } - // deploymentList := appsv1.DeploymentList{} - // err := k8sClient.List(ctx, &deploymentList, labels) - // Expect(err).ToNot(HaveOccurred()) - // for _, deployment := range deploymentList.Items { - // ownerRefs := deployment.GetOwnerReferences() - // Expect(ownerRefs).To(HaveLen(1)) - // Expect(ownerRefs[0].Name).To(Equal("test")) - // } - // }) - // }) - //}) }) diff --git a/internal/crdinstaller/apply.go b/internal/crdinstaller/apply.go new file mode 100644 index 00000000..924aef00 --- /dev/null +++ b/internal/crdinstaller/apply.go @@ -0,0 +1,94 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package crdinstaller + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +// FieldManager is the SSA owner string used for every CRD this binary +// applies. cert-manager's CA injector uses its own field manager +// ("cainjector") and owns the conversion webhook caBundle field, so SSA +// here leaves caBundle alone on re-apply. +const FieldManager = "wandb-crd-installer" + +// Apply installs (or updates) every selected CRD via server-side apply. It is +// idempotent — re-running with the same options is a no-op for CRDs that +// already match. +func Apply(ctx context.Context, opts Options, client apiextensionsclient.Interface, logger *slog.Logger) error { + if err := opts.Validate(); err != nil { + return err + } + if logger == nil { + logger = slog.Default() + } + crds, err := compose(opts) + if err != nil { + return err + } + + for _, crd := range crds { + if err := applyOne(ctx, client, crd, logger); err != nil { + return err + } + } + return nil +} + +func applyOne(ctx context.Context, client apiextensionsclient.Interface, crd *apiextensionsv1.CustomResourceDefinition, logger *slog.Logger) error { + // SSA expects the patch body to declare apiVersion + kind even though + // the typed object already knows them. + crd.APIVersion = apiextensionsv1.SchemeGroupVersion.String() + crd.Kind = "CustomResourceDefinition" + // Strip fields that are populated by the server. Including them in an + // SSA patch either gets rejected or forces ownership of immutable bits. + crd.ResourceVersion = "" + crd.UID = "" + crd.ManagedFields = nil + crd.Generation = 0 + crd.CreationTimestamp = metav1.Time{} + + data, err := json.Marshal(crd) + if err != nil { + return fmt.Errorf("marshalling CRD %s: %w", crd.Name, err) + } + + force := true + applied, err := client.ApiextensionsV1().CustomResourceDefinitions().Patch( + ctx, + crd.Name, + types.ApplyPatchType, + data, + metav1.PatchOptions{ + FieldManager: FieldManager, + Force: &force, + }, + ) + if err != nil { + return fmt.Errorf("applying CRD %s: %w", crd.Name, err) + } + logger.Info("applied CRD", "name", applied.Name, "resourceVersion", applied.ResourceVersion) + return nil +} diff --git a/internal/crdinstaller/compose.go b/internal/crdinstaller/compose.go new file mode 100644 index 00000000..5dc1439f --- /dev/null +++ b/internal/crdinstaller/compose.go @@ -0,0 +1,232 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package crdinstaller + +import ( + "bytes" + "embed" + "fmt" + "io/fs" + "sort" + "strings" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/yaml" +) + +// Options carries the chart-computed values that get plugged into the +// operator-owned CRDs at install time. Upstream CRDs (redis, clickhouse) are +// applied as-is and ignore these fields. +type Options struct { + // CertInjectReference is the verbatim value for the + // cert-manager.io/inject-ca-from annotation on operator-owned CRDs, + // e.g. "wandb/test-serving-cert". Required for operator CRDs. + CertInjectReference string + + // WebhookServiceName is the service name written into + // spec.conversion.webhook.clientConfig.service.name on operator-owned CRDs. + WebhookServiceName string + + // WebhookServiceNamespace is the namespace written into + // spec.conversion.webhook.clientConfig.service.namespace on operator-owned CRDs. + WebhookServiceNamespace string + + // Groups is the set of optional CRD groups to install in addition to + // the operator's own CRDs (e.g. {"redis", "clickhouse"}). Unknown values are + // rejected up-front by ParseGroups. + Groups []string +} + +// Validate ensures every option needed to install operator-owned CRDs is set. +// Optional-group CRDs don't depend on these values. +func (o Options) Validate() error { + var missing []string + if o.CertInjectReference == "" { + missing = append(missing, "cert-inject-reference") + } + if o.WebhookServiceName == "" { + missing = append(missing, "webhook-service-name") + } + if o.WebhookServiceNamespace == "" { + missing = append(missing, "webhook-service-namespace") + } + if len(missing) > 0 { + return fmt.Errorf("missing required option(s): %s", strings.Join(missing, ", ")) + } + return nil +} + +// ParseGroups splits a comma-separated --groups flag value, rejecting unknown +// names so misconfiguration fails fast instead of silently skipping CRDs. +func ParseGroups(raw string) ([]string, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, nil + } + var out []string + for _, part := range strings.Split(raw, ",") { + g := strings.TrimSpace(part) + if g == "" { + continue + } + if _, ok := optionalGroups[g]; !ok { + known := make([]string, 0, len(optionalGroups)) + for k := range optionalGroups { + known = append(known, k) + } + sort.Strings(known) + return nil, fmt.Errorf("unknown CRD group %q (known: %s)", g, strings.Join(known, ", ")) + } + out = append(out, g) + } + return out, nil +} + +// compose walks every selected CRD source in deterministic order, parses each +// CRD, applies operator-owned mutations, and returns the typed objects. The +// order matches what Render emits to stdout and what Apply uses for client +// calls — invariant so debug-time `render | kubectl diff` is meaningful. +func compose(opts Options) ([]*apiextensionsv1.CustomResourceDefinition, error) { + type source struct { + group string + fsys embed.FS + } + sources := []source{{group: "operator", fsys: operatorCRDs}} + for _, g := range opts.Groups { + fsys, ok := optionalGroups[g] + if !ok { + // Defense in depth — ParseGroups already rejects unknowns. + return nil, fmt.Errorf("unknown CRD group %q", g) + } + sources = append(sources, source{group: g, fsys: fsys}) + } + + var crds []*apiextensionsv1.CustomResourceDefinition + for _, src := range sources { + paths, err := listYAMLs(src.fsys) + if err != nil { + return nil, fmt.Errorf("listing %s CRDs: %w", src.group, err) + } + for _, path := range paths { + raw, err := src.fsys.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", path, err) + } + docs, err := splitYAMLDocs(raw) + if err != nil { + return nil, fmt.Errorf("splitting %s: %w", path, err) + } + for _, doc := range docs { + crd := &apiextensionsv1.CustomResourceDefinition{} + if err := yaml.Unmarshal(doc, crd); err != nil { + return nil, fmt.Errorf("unmarshalling CRD from %s: %w", path, err) + } + if crd.Name == "" { + continue // empty doc + } + if src.group == "operator" { + if err := mutateOperatorCRD(crd, opts); err != nil { + return nil, fmt.Errorf("mutating %s: %w", crd.Name, err) + } + } + crds = append(crds, crd) + } + } + } + return crds, nil +} + +// listYAMLs walks fsys and returns every .yaml file path, sorted, so two +// invocations on the same binary produce byte-identical output. +func listYAMLs(fsys embed.FS) ([]string, error) { + var out []string + err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + if strings.HasSuffix(path, ".yaml") { + out = append(out, path) + } + return nil + }) + if err != nil { + return nil, err + } + sort.Strings(out) + return out, nil +} + +// splitYAMLDocs handles multi-document YAML files (separated by `---`). +// CRDs with a leading `---` line produce an empty first document, which we +// filter out by checking for a missing kind in the caller. +func splitYAMLDocs(raw []byte) ([][]byte, error) { + const sep = "\n---" + chunks := bytes.SplitAfter(raw, []byte(sep)) + // SplitAfter keeps the separator in each chunk; trim them and prepend + // `---` removal at the head of each. + var out [][]byte + for _, chunk := range chunks { + c := bytes.TrimSpace(chunk) + // Strip a leading "---" left over from the previous separator. + c = bytes.TrimPrefix(c, []byte("---")) + c = bytes.TrimSpace(c) + if len(c) > 0 { + out = append(out, c) + } + } + return out, nil +} + +// crdsNeedingConversionWebhook lists operator CRDs that require a webhook +// conversion config injected at install time. Add new entries here whenever +// a CRD grows multiple stored versions that need server-side conversion. +var crdsNeedingConversionWebhook = map[string]bool{ + "weightsandbiases.apps.wandb.com": true, +} + +// mutateOperatorCRD plugs the chart-computed values into the operator's own +// CRDs: the cert-manager CA-injection annotation, and (for CRDs that need +// conversion) the webhook conversion service identity. Upstream CRDs are not +// mutated. +func mutateOperatorCRD(crd *apiextensionsv1.CustomResourceDefinition, opts Options) error { + if crd.Annotations == nil { + crd.Annotations = make(map[string]string, 1) + } + crd.Annotations["cert-manager.io/inject-ca-from"] = opts.CertInjectReference + + if crdsNeedingConversionWebhook[crd.Name] { + crd.Spec.Conversion = &apiextensionsv1.CustomResourceConversion{ + Strategy: apiextensionsv1.WebhookConverter, + Webhook: &apiextensionsv1.WebhookConversion{ + ConversionReviewVersions: []string{"v1"}, + ClientConfig: &apiextensionsv1.WebhookClientConfig{ + Service: &apiextensionsv1.ServiceReference{ + Name: opts.WebhookServiceName, + Namespace: opts.WebhookServiceNamespace, + Path: stringPtr("/convert"), + }, + }, + }, + } + } + return nil +} + +func stringPtr(s string) *string { return &s } diff --git a/internal/crdinstaller/compose_test.go b/internal/crdinstaller/compose_test.go new file mode 100644 index 00000000..689de64d --- /dev/null +++ b/internal/crdinstaller/compose_test.go @@ -0,0 +1,146 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +*/ + +package crdinstaller + +import ( + "bytes" + "context" + "strings" + "testing" +) + +var validOpts = Options{ + CertInjectReference: "wandb/test-serving-cert", + WebhookServiceName: "test-wandb-operator", + WebhookServiceNamespace: "wandb", +} + +func TestParseGroupsValid(t *testing.T) { + got, err := ParseGroups("redis,clickhouse") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(got) != 2 || got[0] != "redis" || got[1] != "clickhouse" { + t.Errorf("got %v, want [redis clickhouse]", got) + } +} + +func TestParseGroupsRejectsUnknown(t *testing.T) { + _, err := ParseGroups("redis,bogus") + if err == nil { + t.Fatal("expected error for unknown group") + } + if !strings.Contains(err.Error(), "bogus") { + t.Errorf("expected error mentioning %q, got %v", "bogus", err) + } +} + +func TestParseGroupsEmpty(t *testing.T) { + got, err := ParseGroups("") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != nil { + t.Errorf("expected nil, got %v", got) + } +} + +func TestOptionsValidate(t *testing.T) { + if err := validOpts.Validate(); err != nil { + t.Fatalf("valid opts rejected: %v", err) + } + bad := validOpts + bad.CertInjectReference = "" + if err := bad.Validate(); err == nil { + t.Error("expected error for missing CertInjectReference") + } +} + +func TestComposeOperatorOnly(t *testing.T) { + crds, err := compose(validOpts) + if err != nil { + t.Fatalf("compose failed: %v", err) + } + if len(crds) != 2 { + t.Fatalf("expected 2 operator CRDs, got %d", len(crds)) + } + for _, crd := range crds { + if got := crd.Annotations["cert-manager.io/inject-ca-from"]; got != validOpts.CertInjectReference { + t.Errorf("%s: cert-manager annotation = %q, want %q", crd.Name, got, validOpts.CertInjectReference) + } + if conv := crd.Spec.Conversion; conv != nil && conv.Webhook != nil && conv.Webhook.ClientConfig != nil && conv.Webhook.ClientConfig.Service != nil { + svc := conv.Webhook.ClientConfig.Service + if svc.Name != validOpts.WebhookServiceName || svc.Namespace != validOpts.WebhookServiceNamespace { + t.Errorf("%s: webhook service = %s/%s, want %s/%s", + crd.Name, svc.Namespace, svc.Name, validOpts.WebhookServiceNamespace, validOpts.WebhookServiceName) + } + } + } +} + +func TestComposeIncludesOptionalGroup(t *testing.T) { + opts := validOpts + opts.Groups = []string{"redis"} + crds, err := compose(opts) + if err != nil { + t.Fatalf("compose failed: %v", err) + } + if len(crds) <= 2 { + t.Fatalf("expected >2 CRDs when redis group included, got %d", len(crds)) + } + // Redis CRDs must NOT have the cert-manager annotation we inject for operator CRDs. + for _, crd := range crds { + if strings.HasSuffix(crd.Name, ".redis.redis.opstreelabs.in") { + if _, ok := crd.Annotations["cert-manager.io/inject-ca-from"]; ok { + t.Errorf("redis CRD %s should not have cert-manager annotation", crd.Name) + } + } + } +} + +func TestComposeIncludesClickHouseGroup(t *testing.T) { + opts := validOpts + opts.Groups = []string{"clickhouse"} + crds, err := compose(opts) + if err != nil { + t.Fatalf("compose failed: %v", err) + } + + names := make(map[string]bool, len(crds)) + for _, crd := range crds { + names[crd.Name] = true + } + for _, name := range []string{ + "clickhouseinstallations.clickhouse.altinity.com", + "clickhouseinstallationtemplates.clickhouse.altinity.com", + "clickhouseoperatorconfigurations.clickhouse.altinity.com", + "clickhousekeeperinstallations.clickhouse-keeper.altinity.com", + } { + if !names[name] { + t.Errorf("expected ClickHouse CRD %s to be included", name) + } + } +} + +func TestRenderIsDeterministic(t *testing.T) { + opts := validOpts + opts.Groups = []string{"redis", "clickhouse"} + var a, b bytes.Buffer + if err := Render(context.Background(), opts, &a); err != nil { + t.Fatalf("first render: %v", err) + } + if err := Render(context.Background(), opts, &b); err != nil { + t.Fatalf("second render: %v", err) + } + if !bytes.Equal(a.Bytes(), b.Bytes()) { + t.Fatal("render output differs between invocations") + } +} diff --git a/internal/crdinstaller/crds/clickhouse/clickhouse.altinity.com_clickhouseinstallations.yaml b/internal/crdinstaller/crds/clickhouse/clickhouse.altinity.com_clickhouseinstallations.yaml new file mode 100644 index 00000000..8bba1545 --- /dev/null +++ b/internal/crdinstaller/crds/clickhouse/clickhouse.altinity.com_clickhouseinstallations.yaml @@ -0,0 +1,9468 @@ +# Template Parameters: +# +# KIND=ClickHouseInstallation +# SINGULAR=clickhouseinstallation +# PLURAL=clickhouseinstallations +# SHORT=chi +# OPERATOR_VERSION=0.26.3 +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clickhouseinstallations.clickhouse.altinity.com + labels: + clickhouse.altinity.com/chop: 0.26.3 +spec: + group: clickhouse.altinity.com + scope: Namespaced + names: + kind: ClickHouseInstallation + singular: clickhouseinstallation + plural: clickhouseinstallations + shortNames: + - chi + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: status + type: string + description: Resource status + jsonPath: .status.status + - name: version + type: string + description: Operator version + priority: 1 # show in wide view + jsonPath: .status.chop-version + - name: clusters + type: integer + description: Clusters count + jsonPath: .status.clusters + - name: shards + type: integer + description: Shards count + priority: 1 # show in wide view + jsonPath: .status.shards + - name: hosts + type: integer + description: Hosts count + jsonPath: .status.hosts + - name: taskID + type: string + description: TaskID + priority: 1 # show in wide view + jsonPath: .status.taskID + - name: hosts-completed + type: integer + description: Completed hosts count + jsonPath: .status.hostsCompleted + - name: hosts-updated + type: integer + description: Updated hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsUpdated + - name: hosts-added + type: integer + description: Added hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsAdded + - name: hosts-deleted + type: integer + description: Hosts deleted count + priority: 1 # show in wide view + jsonPath: .status.hostsDeleted + - name: endpoint + type: string + description: Client access endpoint + priority: 1 # show in wide view + jsonPath: .status.endpoint + - name: age + type: date + description: Age of the resource + # Displayed in all priorities + jsonPath: .metadata.creationTimestamp + - name: suspend + type: string + description: Suspend reconciliation + # Displayed in all priorities + jsonPath: .spec.suspend + subresources: + status: {} + schema: + openAPIV3Schema: + description: "define a set of Kubernetes resources (StatefulSet, PVC, Service, ConfigMap) which describe behavior one or more clusters" + type: object + required: + - spec + properties: + apiVersion: + description: | + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: | + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + type: object + description: | + Status contains many fields like a normalized configuration, clickhouse-operator version, current action and all applied action list, current taskID and all applied taskIDs and other + properties: + chop-version: + type: string + description: "Operator version" + chop-commit: + type: string + description: "Operator git commit SHA" + chop-date: + type: string + description: "Operator build date" + chop-ip: + type: string + description: "IP address of the operator's pod which managed this resource" + clusters: + type: integer + minimum: 0 + description: "Clusters count" + shards: + type: integer + minimum: 0 + description: "Shards count" + replicas: + type: integer + minimum: 0 + description: "Replicas count" + hosts: + type: integer + minimum: 0 + description: "Hosts count" + status: + type: string + description: "Status" + taskID: + type: string + description: "Current task id" + taskIDsStarted: + type: array + description: "Started task ids" + nullable: true + items: + type: string + taskIDsCompleted: + type: array + description: "Completed task ids" + nullable: true + items: + type: string + action: + type: string + description: "Action" + actions: + type: array + description: "Actions" + nullable: true + items: + type: string + error: + type: string + description: "Last error" + errors: + type: array + description: "Errors" + nullable: true + items: + type: string + hostsUnchanged: + type: integer + minimum: 0 + description: "Unchanged Hosts count" + hostsUpdated: + type: integer + minimum: 0 + description: "Updated Hosts count" + hostsAdded: + type: integer + minimum: 0 + description: "Added Hosts count" + hostsCompleted: + type: integer + minimum: 0 + description: "Completed Hosts count" + hostsDeleted: + type: integer + minimum: 0 + description: "Deleted Hosts count" + hostsDelete: + type: integer + minimum: 0 + description: "About to delete Hosts count" + pods: + type: array + description: "Pods" + nullable: true + items: + type: string + pod-ips: + type: array + description: "Pod IPs" + nullable: true + items: + type: string + fqdns: + type: array + description: "Pods FQDNs" + nullable: true + items: + type: string + endpoint: + type: string + description: "Endpoint" + endpoints: + type: array + description: "All endpoints" + nullable: true + items: + type: string + generation: + type: integer + minimum: 0 + description: "Generation" + normalized: + type: object + description: "Normalized resource requested" + nullable: true + x-kubernetes-preserve-unknown-fields: true + normalizedCompleted: + type: object + description: "Normalized resource completed" + nullable: true + x-kubernetes-preserve-unknown-fields: true + actionPlan: + type: object + description: "Action Plan" + nullable: true + x-kubernetes-preserve-unknown-fields: true + hostsWithTablesCreated: + type: array + description: "List of hosts with tables created by the operator" + nullable: true + items: + type: string + hostsWithReplicaCaughtUp: + type: array + description: "List of hosts with replica caught up" + nullable: true + items: + type: string + usedTemplates: + type: array + description: "List of templates used to build this CHI" + nullable: true + x-kubernetes-preserve-unknown-fields: true + items: + type: object + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + # x-kubernetes-preserve-unknown-fields: true + description: | + Specification of the desired behavior of one or more ClickHouse clusters + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md + properties: + taskID: + type: string + description: | + Allows to define custom taskID for CHI update and watch status of this update execution. + Displayed in all .status.taskID* fields. + By default (if not filled) every update of CHI manifest will generate random taskID + stop: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + restart: + type: string + description: | + In case 'RollingUpdate' specified, the operator will always restart ClickHouse pods during reconcile. + This options is used in rare cases when force restart is required and is typically removed after the use in order to avoid unneeded restarts. + enum: + - "" + - "RollingUpdate" + suspend: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Suspend reconciliation of resources managed by a ClickHouse Installation. + Works as the following: + - When `suspend` is `true` operator stops reconciling all resources. + - When `suspend` is `false` or not set, operator reconciles all resources. + troubleshoot: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Allows to troubleshoot Pods during CrashLoopBack state. + This may happen when wrong configuration applied, in this case `clickhouse-server` wouldn't start. + Command within ClickHouse container is modified with `sleep` in order to avoid quick restarts + and give time to troubleshoot via CLI. + Liveness and Readiness probes are disabled as well. + namespaceDomainPattern: + type: string + description: | + Custom domain pattern which will be used for DNS names of `Service` or `Pod`. + Typical use scenario - custom cluster domain in Kubernetes cluster + Example: %s.svc.my.test + templating: + type: object + # nullable: true + description: | + Optional, applicable inside ClickHouseInstallationTemplate only. + Defines current ClickHouseInstallationTemplate application options to target ClickHouseInstallation(s)." + properties: + policy: + type: string + description: | + When defined as `auto` inside ClickhouseInstallationTemplate, this ClickhouseInstallationTemplate + will be auto-added into ClickHouseInstallation, selectable by `chiSelector`. + Default value is `manual`, meaning ClickHouseInstallation should request this ClickhouseInstallationTemplate explicitly. + enum: + - "" + - "auto" + - "manual" + chiSelector: + type: object + description: "Optional, defines selector for ClickHouseInstallation(s) to be templated with ClickhouseInstallationTemplate" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + reconciling: + type: object + description: "[OBSOLETED] Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + # nullable: true + properties: + policy: + type: string + description: | + DISCUSSED TO BE DEPRECATED + Syntax sugar + Overrides all three 'reconcile.host.wait.{exclude, queries, include}' values from the operator's config + Possible values: + - wait - should wait to exclude host, complete queries and include host back into the cluster + - nowait - should NOT wait to exclude host, complete queries and include host back into the cluster + enum: + - "" + - "wait" + - "nowait" + configMapPropagationTimeout: + type: integer + description: | + Timeout in seconds for `clickhouse-operator` to wait for modified `ConfigMap` to propagate into the `Pod` + More details: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically + minimum: 0 + maximum: 3600 + cleanup: + type: object + description: "Optional, defines behavior for cleanup Kubernetes resources during reconcile cycle" + # nullable: true + properties: + unknownObjects: + type: object + description: | + Describes what clickhouse-operator should do with found Kubernetes resources which should be managed by clickhouse-operator, + but do not have `ownerReference` to any currently managed `ClickHouseInstallation` resource. + Default behavior is `Delete`" + # nullable: true + properties: + statefulSet: + type: string + description: "Behavior policy for unknown StatefulSet, `Delete` by default" + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown PVC, `Delete` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown ConfigMap, `Delete` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown Service, `Delete` by default" + reconcileFailedObjects: + type: object + description: | + Describes what clickhouse-operator should do with Kubernetes resources which are failed during reconcile. + Default behavior is `Retain`" + # nullable: true + properties: + statefulSet: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed StatefulSet, `Retain` by default" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed PVC, `Retain` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed ConfigMap, `Retain` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed Service, `Retain` by default" + macros: + type: object + description: "macros parameters" + properties: + sections: + type: object + description: "sections behaviour for macros" + properties: + users: + type: object + description: "sections behaviour for macros on users" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + profiles: + type: object + description: "sections behaviour for macros on profiles" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + quotas: + type: object + description: "sections behaviour for macros on quotas" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + settings: + type: object + description: "sections behaviour for macros on settings" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + files: + type: object + description: "sections behaviour for macros on files" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + statefulSet: + type: object + description: "Optional, StatefulSet reconcile behavior tuning" + properties: + create: + type: object + description: "Behavior during create StatefulSet" + properties: + onFailure: + type: string + description: | + What to do in case created StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. delete - delete newly created problematic StatefulSet and follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "delete" + - "ignore" + update: + type: object + description: "Behavior during update StatefulSet" + properties: + timeout: + type: integer + description: "How many seconds to wait for StatefulSet to be 'Ready' during update" + minimum: 0 + maximum: 3600 + pollInterval: + type: integer + description: "How many seconds to wait between checks for StatefulSet status during update" + minimum: 1 + maximum: 600 + onFailure: + type: string + description: | + What to do in case updated StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. rollback - delete Pod and rollback StatefulSet to previous Generation. Follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "rollback" + - "ignore" + recreate: + type: object + description: "Behavior during recreate StatefulSet" + properties: + onDataLoss: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to PVC data loss or missing volumes. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + onUpdateFailure: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to update failure or StatefulSet not ready. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + reconcile: + type: object + # nullable: true + properties: + policy: + type: string + description: | + DISCUSSED TO BE DEPRECATED + Syntax sugar + Overrides all three 'reconcile.host.wait.{exclude, queries, include}' values from the operator's config + Possible values: + - wait - should wait to exclude host, complete queries and include host back into the cluster + - nowait - should NOT wait to exclude host, complete queries and include host back into the cluster + enum: + - "" + - "wait" + - "nowait" + configMapPropagationTimeout: + type: integer + description: | + Timeout in seconds for `clickhouse-operator` to wait for modified `ConfigMap` to propagate into the `Pod` + More details: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically + minimum: 0 + maximum: 3600 + cleanup: + type: object + description: "Optional, defines behavior for cleanup Kubernetes resources during reconcile cycle" + # nullable: true + properties: + unknownObjects: + type: object + description: | + Describes what clickhouse-operator should do with found Kubernetes resources which should be managed by clickhouse-operator, + but do not have `ownerReference` to any currently managed `ClickHouseInstallation` resource. + Default behavior is `Delete`" + # nullable: true + properties: + statefulSet: + type: string + description: "Behavior policy for unknown StatefulSet, `Delete` by default" + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown PVC, `Delete` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown ConfigMap, `Delete` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown Service, `Delete` by default" + reconcileFailedObjects: + type: object + description: | + Describes what clickhouse-operator should do with Kubernetes resources which are failed during reconcile. + Default behavior is `Retain`" + # nullable: true + properties: + statefulSet: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed StatefulSet, `Retain` by default" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed PVC, `Retain` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed ConfigMap, `Retain` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed Service, `Retain` by default" + macros: + type: object + description: "macros parameters" + properties: + sections: + type: object + description: "sections behaviour for macros" + properties: + users: + type: object + description: "sections behaviour for macros on users" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + profiles: + type: object + description: "sections behaviour for macros on profiles" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + quotas: + type: object + description: "sections behaviour for macros on quotas" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + settings: + type: object + description: "sections behaviour for macros on settings" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + files: + type: object + description: "sections behaviour for macros on files" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + statefulSet: + type: object + description: "Optional, StatefulSet reconcile behavior tuning" + properties: + create: + type: object + description: "Behavior during create StatefulSet" + properties: + onFailure: + type: string + description: | + What to do in case created StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. delete - delete newly created problematic StatefulSet and follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "delete" + - "ignore" + update: + type: object + description: "Behavior during update StatefulSet" + properties: + timeout: + type: integer + description: "How many seconds to wait for StatefulSet to be 'Ready' during update" + minimum: 0 + maximum: 3600 + pollInterval: + type: integer + description: "How many seconds to wait between checks for StatefulSet status during update" + minimum: 1 + maximum: 600 + onFailure: + type: string + description: | + What to do in case updated StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. rollback - delete Pod and rollback StatefulSet to previous Generation. Follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "rollback" + - "ignore" + recreate: + type: object + description: "Behavior during recreate StatefulSet" + properties: + onDataLoss: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to PVC data loss or missing volumes. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + onUpdateFailure: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to update failure or StatefulSet not ready. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + description: "Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + defaults: + type: object + description: | + define default behavior for whole ClickHouseInstallation, some behavior can be re-define on cluster, shard and replica level + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specdefaults + # nullable: true + properties: + replicasUseFQDN: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + define should replicas be specified by FQDN in `<host></host>`. + In case of "no" will use short hostname and clickhouse-server will use kubernetes default suffixes for DNS lookup + "no" by default + distributedDDL: + type: object + description: | + allows change `<yandex><distributed_ddl></distributed_ddl></yandex>` settings + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings-distributed_ddl + # nullable: true + properties: + profile: + type: string + description: "Settings from this profile will be used to execute DDL queries" + storageManagement: + type: object + description: default storage management options + properties: + provisioner: + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + templates: + type: object + description: "optional, configuration of the templates names which will use for generate Kubernetes resources according to one or more ClickHouse clusters described in current ClickHouseInstallation (chi) resource" + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + configuration: + type: object + description: "allows configure multiple aspects and behavior for `clickhouse-server` instance and also allows describe multiple `clickhouse-server` clusters inside one `chi` resource" + # nullable: true + properties: + zookeeper: + type: object + description: | + allows configure <yandex><zookeeper>..</zookeeper></yandex> section in each `Pod` during generate `ConfigMap` which will mounted in `/etc/clickhouse-server/config.d/` + `clickhouse-operator` itself doesn't manage Zookeeper, please install Zookeeper separatelly look examples on https://github.com/Altinity/clickhouse-operator/tree/master/deploy/zookeeper/ + currently, zookeeper (or clickhouse-keeper replacement) used for *ReplicatedMergeTree table engines and for `distributed_ddl` + More details: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings_zookeeper + # nullable: true + properties: + nodes: + type: array + description: "describe every available zookeeper cluster node for interaction" + # nullable: true + items: + type: object + #required: + # - host + properties: + host: + type: string + description: "dns name or ip address for Zookeeper node" + port: + type: integer + description: "TCP port which used to connect to Zookeeper node" + minimum: 0 + maximum: 65535 + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "if a secure connection to Zookeeper is required" + availabilityZone: + type: string + description: "availability zone for Zookeeper node" + session_timeout_ms: + type: integer + description: "session timeout during connect to Zookeeper" + operation_timeout_ms: + type: integer + description: "one operation timeout during Zookeeper transactions" + root: + type: string + description: "optional root znode path inside zookeeper to store ClickHouse related data (replication queue or distributed DDL)" + identity: + type: string + description: "optional access credentials string with `user:password` format used when use digest authorization in Zookeeper" + use_compression: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Enables compression in Keeper protocol if set to true" + users: + type: object + description: | + allows configure <yandex><users>..</users></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure password hashed, authorization restrictions, database level security row filters etc. + More details: https://clickhouse.tech/docs/en/operations/settings/settings-users/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationusers + + any key could contains `valueFrom` with `secretKeyRef` which allow pass password from kubernetes secrets + secret value will pass in `pod.spec.containers.evn`, and generate with from_env=XXX in XML in /etc/clickhouse-server/users.d/chop-generated-users.xml + it not allow automatically updates when updates `secret`, change spec.taskID for manually trigger reconcile cycle + + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + any key with prefix `k8s_secret_` shall has value with format namespace/secret/key or secret/key + in this case value from secret will write directly into XML tag during render *-usersd ConfigMap + + any key with prefix `k8s_secret_env` shall has value with format namespace/secret/key or secret/key + in this case value from secret will write into environment variable and write to XML tag via from_env=XXX + + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + # nullable: true + x-kubernetes-preserve-unknown-fields: true + profiles: + type: object + description: | + allows configure <yandex><profiles>..</profiles></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure any aspect of settings profile + More details: https://clickhouse.tech/docs/en/operations/settings/settings-profiles/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationprofiles + # nullable: true + x-kubernetes-preserve-unknown-fields: true + quotas: + type: object + description: | + allows configure <yandex><quotas>..</quotas></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure any aspect of resource quotas + More details: https://clickhouse.tech/docs/en/operations/quotas/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationquotas + # nullable: true + x-kubernetes-preserve-unknown-fields: true + settings: + type: object + description: | + allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationsettings + + any key could contains `valueFrom` with `secretKeyRef` which allow pass password from kubernetes secrets + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + secret value will pass in `pod.spec.env`, and generate with from_env=XXX in XML in /etc/clickhouse-server/config.d/chop-generated-settings.xml + it not allow automatically updates when updates `secret`, change spec.taskID for manually trigger reconcile cycle + # nullable: true + x-kubernetes-preserve-unknown-fields: true + files: + type: object + description: | + allows define content of any setting file inside each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + every key in this object is the file name + every value in this object is the file content + you can use `!!binary |` and base64 for binary files, see details here https://yaml.org/type/binary.html + each key could contains prefix like {common}, {users}, {hosts} or config.d, users.d, conf.d, wrong prefixes will be ignored, subfolders also will be ignored + More details: https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-05-files-nested.yaml + + any key could contains `valueFrom` with `secretKeyRef` which allow pass values from kubernetes secrets + secrets will mounted into pod as separate volume in /etc/clickhouse-server/secrets.d/ + and will automatically update when update secret + it useful for pass SSL certificates from cert-manager or similar tool + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + # nullable: true + x-kubernetes-preserve-unknown-fields: true + clusters: + type: array + description: | + describes clusters layout and allows change settings on cluster-level, shard-level and replica-level + every cluster is a set of StatefulSet, one StatefulSet contains only one Pod with `clickhouse-server` + all Pods will rendered in <remote_server> part of ClickHouse configs, mounted from ConfigMap as `/etc/clickhouse-server/config.d/chop-generated-remote_servers.xml` + Clusters will use for Distributed table engine, more details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + If `cluster` contains zookeeper settings (could be inherited from top `chi` level), when you can create *ReplicatedMergeTree tables + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "cluster name, used to identify set of servers and wide used during generate names of related Kubernetes resources" + minLength: 1 + # See namePartClusterMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + zookeeper: + type: object + # nullable: true + properties: + nodes: + type: array + description: "describe every available zookeeper cluster node for interaction" + # nullable: true + items: + type: object + #required: + # - host + properties: + host: + type: string + description: "dns name or ip address for Zookeeper node" + port: + type: integer + description: "TCP port which used to connect to Zookeeper node" + minimum: 0 + maximum: 65535 + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "if a secure connection to Zookeeper is required" + availabilityZone: + type: string + description: "availability zone for Zookeeper node" + session_timeout_ms: + type: integer + description: "session timeout during connect to Zookeeper" + operation_timeout_ms: + type: integer + description: "one operation timeout during Zookeeper transactions" + root: + type: string + description: "optional root znode path inside zookeeper to store ClickHouse related data (replication queue or distributed DDL)" + identity: + type: string + description: "optional access credentials string with `user:password` format used when use digest authorization in Zookeeper" + use_compression: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Enables compression in Keeper protocol if set to true" + description: | + optional, allows configure <yandex><zookeeper>..</zookeeper></yandex> section in each `Pod` only in current ClickHouse cluster, during generate `ConfigMap` which will mounted in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.zookeeper` settings + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` on current cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected cluster + override top-level `chi.spec.configuration.templates` + schemaPolicy: + type: object + description: | + describes how schema is propagated within replicas and shards + properties: + replica: + type: string + description: "how schema is propagated within a replica" + enum: + # List SchemaPolicyReplicaXXX constants from model + - "" + - "None" + - "All" + shard: + type: string + description: "how schema is propagated between shards" + enum: + # List SchemaPolicyShardXXX constants from model + - "" + - "None" + - "All" + - "DistributedTablesOnly" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: optional, open secure ports for cluster + secret: + type: object + description: "optional, shared secret value to secure cluster communications" + properties: + auto: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Auto-generate shared secret value to secure cluster communications" + value: + description: "Cluster shared secret value in plain text" + type: string + valueFrom: + description: "Cluster shared secret source" + type: object + properties: + secretKeyRef: + description: | + Selects a key of a secret in the clickhouse installation namespace. + Should not be used if value is not empty. + type: object + properties: + name: + description: | + Name of the referent. More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + key: + description: The key of the secret to select from. Must be a valid secret key. + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - name + - key + pdbManaged: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Specifies whether the Pod Disruption Budget (PDB) should be managed. + During the next installation, if PDB management is enabled, the operator will + attempt to retrieve any existing PDB. If none is found, it will create a new one + and initiate a reconciliation loop. If PDB management is disabled, the existing PDB + will remain intact, and the reconciliation loop will not be executed. By default, + PDB management is enabled. + pdbMaxUnavailable: + type: integer + description: | + Pod eviction is allowed if at most "pdbMaxUnavailable" pods are unavailable after the eviction, + i.e. even in absence of the evicted pod. For example, one can prevent all voluntary evictions + by specifying 0. This is a mutually exclusive setting with "minAvailable". + minimum: 0 + maximum: 65535 + reconcile: + type: object + description: "allow tuning reconciling process" + properties: + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + layout: + type: object + description: | + describe current cluster layout, how much shards in cluster, how much replica in shard + allows override settings on each shard and replica separatelly + # nullable: true + properties: + shardsCount: + type: integer + description: | + how much shards for current ClickHouse cluster will run in Kubernetes, + each shard contains shared-nothing part of data and contains set of replicas, + cluster contains 1 shard by default" + replicasCount: + type: integer + description: | + how much replicas in each shards for current cluster will run in Kubernetes, + each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + every shard contains 1 replica by default" + shards: + type: array + description: | + optional, allows override top-level `chi.spec.configuration`, cluster-level + `chi.spec.configuration.clusters` settings for each shard separately, + use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + definitionType: + type: string + description: "DEPRECATED - to be removed soon" + weight: + type: integer + description: | + optional, 1 by default, allows setup shard <weight> setting which will use during insert into tables with `Distributed` engine, + will apply in <remote_servers> inside ConfigMap which will mount in /etc/clickhouse-server/config.d/chop-generated-remote_servers.xml + More details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + internalReplication: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, `true` by default when `chi.spec.configuration.clusters[].layout.ReplicaCount` > 1 and 0 otherwise + allows setup <internal_replication> setting which will use during insert into tables with `Distributed` engine for insert only in one live replica and other replicas will download inserted data during replication, + will apply in <remote_servers> inside ConfigMap which will mount in /etc/clickhouse-server/config.d/chop-generated-remote_servers.xml + More details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one shard during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` and cluster-level `chi.spec.configuration.clusters.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected shard + override top-level `chi.spec.configuration.templates` and cluster-level `chi.spec.configuration.clusters.templates` + replicasCount: + type: integer + description: | + optional, how much replicas in selected shard for selected ClickHouse cluster will run in Kubernetes, each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + shard contains 1 replica by default + override cluster-level `chi.spec.configuration.clusters.layout.replicasCount` + minimum: 1 + replicas: + type: array + description: | + optional, allows override behavior for selected replicas from cluster-level `chi.spec.configuration.clusters` and shard-level `chi.spec.configuration.clusters.layout.shards` + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `tcp` for selected replica, override `chi.spec.templates.hostTemplates.spec.tcpPort` + allows connect to `clickhouse-server` via TCP Native protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `http` for selected replica, override `chi.spec.templates.hostTemplates.spec.httpPort` + allows connect to `clickhouse-server` via HTTP protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `interserver` for selected replica, override `chi.spec.templates.hostTemplates.spec.interserverHTTPPort` + allows connect between replicas inside same shard during fetch replicated data parts HTTP protocol + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and shard-level `chi.spec.configuration.clusters.layout.shards.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files`, cluster-level `chi.spec.configuration.clusters.files` and shard-level `chi.spec.configuration.clusters.layout.shards.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` and shard-level `chi.spec.configuration.clusters.layout.shards.templates` + replicas: + type: array + description: "optional, allows override top-level `chi.spec.configuration` and cluster-level `chi.spec.configuration.clusters` configuration for each replica and each shard relates to selected replica, use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and will ignore if shard-level `chi.spec.configuration.clusters.layout.shards` present + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` + shardsCount: + type: integer + description: "optional, count of shards related to current replica, you can override each shard behavior on low-level `chi.spec.configuration.clusters.layout.replicas.shards`" + minimum: 1 + shards: + type: array + description: "optional, list of shards related to current replica, will ignore if `chi.spec.configuration.clusters.layout.shards` presents" + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `tcp` for selected shard, override `chi.spec.templates.hostTemplates.spec.tcpPort` + allows connect to `clickhouse-server` via TCP Native protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `http` for selected shard, override `chi.spec.templates.hostTemplates.spec.httpPort` + allows connect to `clickhouse-server` via HTTP protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `interserver` for selected shard, override `chi.spec.templates.hostTemplates.spec.interserverHTTPPort` + allows connect between replicas inside same shard during fetch replicated data parts HTTP protocol + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and replica-level `chi.spec.configuration.clusters.layout.replicas.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates` + templates: + type: object + description: "allows define templates which will use for render Kubernetes resources like StatefulSet, ConfigMap, Service, PVC, by default, clickhouse-operator have own templates, but you can override it" + # nullable: true + properties: + hostTemplates: + type: array + description: "hostTemplate will use during apply to generate `clickhose-server` config files" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.hostTemplate`, cluster-level `chi.spec.configuration.clusters.templates.hostTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.hostTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.hostTemplate`" + type: string + portDistribution: + type: array + description: "define how will distribute numeric values of named ports in `Pod.spec.containers.ports` and clickhouse-server configs" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "type of distribution, when `Unspecified` (default value) then all listen ports on clickhouse-server configuration in all Pods will have the same value, when `ClusterScopeIndex` then ports will increment to offset from base value depends on shard and replica index inside cluster with combination of `chi.spec.templates.podTemlates.spec.HostNetwork` it allows setup ClickHouse cluster inside Kubernetes and provide access via external network bypass Kubernetes internal network" + enum: + # List PortDistributionXXX constants + - "" + - "Unspecified" + - "ClusterScopeIndex" + spec: + # Host + type: object + properties: + name: + type: string + description: "by default, hostname will generate, but this allows define custom name for each `clickhouse-server`" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `tcp_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=tcp]` + More info: https://clickhouse.tech/docs/en/interfaces/tcp/ + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `http_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=http]` + More info: https://clickhouse.tech/docs/en/interfaces/http/ + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `interserver_http_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=interserver]` + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#interserver-http-port + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: "be careful, this part of CRD allows override template inside template, don't use it if you don't understand what you do" + podTemplates: + type: array + description: | + podTemplate will use during render `Pod` inside `StatefulSet.spec` and allows define rendered `Pod.spec`, pod scheduling distribution and pod zone + More information: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatespodtemplates + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.podTemplate`, cluster-level `chi.spec.configuration.clusters.templates.podTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.podTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.podTemplate`" + generateName: + type: string + description: "allows define format for generated `Pod` name, look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates for details about available template variables" + zone: + type: object + description: "allows define custom zone name and will separate ClickHouse `Pods` between nodes, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + #required: + # - values + properties: + key: + type: string + description: "optional, if defined, allows select kubernetes nodes by label with `name` equal `key`" + values: + type: array + description: "optional, if defined, allows select kubernetes nodes by label with `value` in `values`" + # nullable: true + items: + type: string + distribution: + type: string + description: "DEPRECATED, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + enum: + - "" + - "Unspecified" + - "OnePerHost" + podDistribution: + type: array + description: "define ClickHouse Pod distribution policy between Kubernetes Nodes inside Shard, Replica, Namespace, CHI, another ClickHouse cluster" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "you can define multiple affinity policy types" + enum: + # List PodDistributionXXX constants + - "" + - "Unspecified" + - "ClickHouseAntiAffinity" + - "ShardAntiAffinity" + - "ReplicaAntiAffinity" + - "AnotherNamespaceAntiAffinity" + - "AnotherClickHouseInstallationAntiAffinity" + - "AnotherClusterAntiAffinity" + - "MaxNumberPerNode" + - "NamespaceAffinity" + - "ClickHouseInstallationAffinity" + - "ClusterAffinity" + - "ShardAffinity" + - "ReplicaAffinity" + - "PreviousTailAffinity" + - "CircularReplication" + scope: + type: string + description: "scope for apply each podDistribution" + enum: + # list PodDistributionScopeXXX constants + - "" + - "Unspecified" + - "Shard" + - "Replica" + - "Cluster" + - "ClickHouseInstallation" + - "Namespace" + number: + type: integer + description: "define, how much ClickHouse Pods could be inside selected scope with selected distribution type" + minimum: 0 + maximum: 65535 + topologyKey: + type: string + description: | + use for inter-pod affinity look to `pod.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution.podAffinityTerm.topologyKey`, + more info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity" + metadata: + type: object + description: | + allows pass standard object's metadata from template to Pod + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify PodSpec + type: object + description: "allows define whole Pod.spec inside StaefulSet.spec, look to https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates for details" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + volumeClaimTemplates: + type: array + description: | + allows define template for rendering `PVC` kubernetes resource, which would use inside `Pod` for mount clickhouse `data`, clickhouse `logs` or something else + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + top-level `chi.spec.defaults.templates.dataVolumeClaimTemplate` or `chi.spec.defaults.templates.logVolumeClaimTemplate`, + cluster-level `chi.spec.configuration.clusters.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.templates.logVolumeClaimTemplate`, + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.shards.temlates.logVolumeClaimTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.replicas.templates.logVolumeClaimTemplate` + provisioner: + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + metadata: + type: object + description: | + allows to pass standard object's metadata from template to PVC + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + description: | + allows define all aspects of `PVC` resource + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims + # nullable: true + x-kubernetes-preserve-unknown-fields: true + serviceTemplates: + type: array + description: | + allows define template for rendering `Service` which would get endpoint from Pods which scoped chi-wide, cluster-wide, shard-wide, replica-wide level + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + chi-level `chi.spec.defaults.templates.serviceTemplate` + cluster-level `chi.spec.configuration.clusters.templates.clusterServiceTemplate` + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.shardServiceTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.replicaServiceTemplate` or `chi.spec.configuration.clusters.layout.shards.replicas.replicaServiceTemplate` + generateName: + type: string + description: | + allows define format for generated `Service` name, + look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates + for details about available template variables" + metadata: + # TODO specify ObjectMeta + type: object + description: | + allows pass standard object's metadata from template to Service + Could be use for define specificly for Cloud Provider metadata which impact to behavior of service + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify ServiceSpec + type: object + description: | + describe behavior of generated Service + More info: https://kubernetes.io/docs/concepts/services-networking/service/ + # nullable: true + x-kubernetes-preserve-unknown-fields: true + useTemplates: + type: array + description: | + list of `ClickHouseInstallationTemplate` (chit) resource names which will merge with current `CHI` + manifest during render Kubernetes resources to create related ClickHouse clusters" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "name of `ClickHouseInstallationTemplate` (chit) resource" + namespace: + type: string + description: "Kubernetes namespace where need search `chit` resource, depending on `watchNamespaces` settings in `clickhouse-operator`" + useType: + type: string + description: "optional, current strategy is only merge, and current `chi` settings have more priority than merged template `chit`" + enum: + # List useTypeXXX constants from model + - "" + - "merge" +--- +# Template Parameters: +# +# KIND=ClickHouseInstallationTemplate +# SINGULAR=clickhouseinstallationtemplate +# PLURAL=clickhouseinstallationtemplates +# SHORT=chit +# OPERATOR_VERSION=0.26.3 +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clickhouseinstallationtemplates.clickhouse.altinity.com + labels: + clickhouse.altinity.com/chop: 0.26.3 +spec: + group: clickhouse.altinity.com + scope: Namespaced + names: + kind: ClickHouseInstallationTemplate + singular: clickhouseinstallationtemplate + plural: clickhouseinstallationtemplates + shortNames: + - chit + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: status + type: string + description: Resource status + jsonPath: .status.status + - name: version + type: string + description: Operator version + priority: 1 # show in wide view + jsonPath: .status.chop-version + - name: clusters + type: integer + description: Clusters count + jsonPath: .status.clusters + - name: shards + type: integer + description: Shards count + priority: 1 # show in wide view + jsonPath: .status.shards + - name: hosts + type: integer + description: Hosts count + jsonPath: .status.hosts + - name: taskID + type: string + description: TaskID + priority: 1 # show in wide view + jsonPath: .status.taskID + - name: hosts-completed + type: integer + description: Completed hosts count + jsonPath: .status.hostsCompleted + - name: hosts-updated + type: integer + description: Updated hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsUpdated + - name: hosts-added + type: integer + description: Added hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsAdded + - name: hosts-deleted + type: integer + description: Hosts deleted count + priority: 1 # show in wide view + jsonPath: .status.hostsDeleted + - name: endpoint + type: string + description: Client access endpoint + priority: 1 # show in wide view + jsonPath: .status.endpoint + - name: age + type: date + description: Age of the resource + # Displayed in all priorities + jsonPath: .metadata.creationTimestamp + - name: suspend + type: string + description: Suspend reconciliation + # Displayed in all priorities + jsonPath: .spec.suspend + subresources: + status: {} + schema: + openAPIV3Schema: + description: "define a set of Kubernetes resources (StatefulSet, PVC, Service, ConfigMap) which describe behavior one or more clusters" + type: object + required: + - spec + properties: + apiVersion: + description: | + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: | + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + type: object + description: | + Status contains many fields like a normalized configuration, clickhouse-operator version, current action and all applied action list, current taskID and all applied taskIDs and other + properties: + chop-version: + type: string + description: "Operator version" + chop-commit: + type: string + description: "Operator git commit SHA" + chop-date: + type: string + description: "Operator build date" + chop-ip: + type: string + description: "IP address of the operator's pod which managed this resource" + clusters: + type: integer + minimum: 0 + description: "Clusters count" + shards: + type: integer + minimum: 0 + description: "Shards count" + replicas: + type: integer + minimum: 0 + description: "Replicas count" + hosts: + type: integer + minimum: 0 + description: "Hosts count" + status: + type: string + description: "Status" + taskID: + type: string + description: "Current task id" + taskIDsStarted: + type: array + description: "Started task ids" + nullable: true + items: + type: string + taskIDsCompleted: + type: array + description: "Completed task ids" + nullable: true + items: + type: string + action: + type: string + description: "Action" + actions: + type: array + description: "Actions" + nullable: true + items: + type: string + error: + type: string + description: "Last error" + errors: + type: array + description: "Errors" + nullable: true + items: + type: string + hostsUnchanged: + type: integer + minimum: 0 + description: "Unchanged Hosts count" + hostsUpdated: + type: integer + minimum: 0 + description: "Updated Hosts count" + hostsAdded: + type: integer + minimum: 0 + description: "Added Hosts count" + hostsCompleted: + type: integer + minimum: 0 + description: "Completed Hosts count" + hostsDeleted: + type: integer + minimum: 0 + description: "Deleted Hosts count" + hostsDelete: + type: integer + minimum: 0 + description: "About to delete Hosts count" + pods: + type: array + description: "Pods" + nullable: true + items: + type: string + pod-ips: + type: array + description: "Pod IPs" + nullable: true + items: + type: string + fqdns: + type: array + description: "Pods FQDNs" + nullable: true + items: + type: string + endpoint: + type: string + description: "Endpoint" + endpoints: + type: array + description: "All endpoints" + nullable: true + items: + type: string + generation: + type: integer + minimum: 0 + description: "Generation" + normalized: + type: object + description: "Normalized resource requested" + nullable: true + x-kubernetes-preserve-unknown-fields: true + normalizedCompleted: + type: object + description: "Normalized resource completed" + nullable: true + x-kubernetes-preserve-unknown-fields: true + actionPlan: + type: object + description: "Action Plan" + nullable: true + x-kubernetes-preserve-unknown-fields: true + hostsWithTablesCreated: + type: array + description: "List of hosts with tables created by the operator" + nullable: true + items: + type: string + hostsWithReplicaCaughtUp: + type: array + description: "List of hosts with replica caught up" + nullable: true + items: + type: string + usedTemplates: + type: array + description: "List of templates used to build this CHI" + nullable: true + x-kubernetes-preserve-unknown-fields: true + items: + type: object + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + # x-kubernetes-preserve-unknown-fields: true + description: | + Specification of the desired behavior of one or more ClickHouse clusters + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md + properties: + taskID: + type: string + description: | + Allows to define custom taskID for CHI update and watch status of this update execution. + Displayed in all .status.taskID* fields. + By default (if not filled) every update of CHI manifest will generate random taskID + stop: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + restart: + type: string + description: | + In case 'RollingUpdate' specified, the operator will always restart ClickHouse pods during reconcile. + This options is used in rare cases when force restart is required and is typically removed after the use in order to avoid unneeded restarts. + enum: + - "" + - "RollingUpdate" + suspend: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Suspend reconciliation of resources managed by a ClickHouse Installation. + Works as the following: + - When `suspend` is `true` operator stops reconciling all resources. + - When `suspend` is `false` or not set, operator reconciles all resources. + troubleshoot: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Allows to troubleshoot Pods during CrashLoopBack state. + This may happen when wrong configuration applied, in this case `clickhouse-server` wouldn't start. + Command within ClickHouse container is modified with `sleep` in order to avoid quick restarts + and give time to troubleshoot via CLI. + Liveness and Readiness probes are disabled as well. + namespaceDomainPattern: + type: string + description: | + Custom domain pattern which will be used for DNS names of `Service` or `Pod`. + Typical use scenario - custom cluster domain in Kubernetes cluster + Example: %s.svc.my.test + templating: + type: object + # nullable: true + description: | + Optional, applicable inside ClickHouseInstallationTemplate only. + Defines current ClickHouseInstallationTemplate application options to target ClickHouseInstallation(s)." + properties: + policy: + type: string + description: | + When defined as `auto` inside ClickhouseInstallationTemplate, this ClickhouseInstallationTemplate + will be auto-added into ClickHouseInstallation, selectable by `chiSelector`. + Default value is `manual`, meaning ClickHouseInstallation should request this ClickhouseInstallationTemplate explicitly. + enum: + - "" + - "auto" + - "manual" + chiSelector: + type: object + description: "Optional, defines selector for ClickHouseInstallation(s) to be templated with ClickhouseInstallationTemplate" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + reconciling: + type: object + description: "[OBSOLETED] Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + # nullable: true + properties: + policy: + type: string + description: | + DISCUSSED TO BE DEPRECATED + Syntax sugar + Overrides all three 'reconcile.host.wait.{exclude, queries, include}' values from the operator's config + Possible values: + - wait - should wait to exclude host, complete queries and include host back into the cluster + - nowait - should NOT wait to exclude host, complete queries and include host back into the cluster + enum: + - "" + - "wait" + - "nowait" + configMapPropagationTimeout: + type: integer + description: | + Timeout in seconds for `clickhouse-operator` to wait for modified `ConfigMap` to propagate into the `Pod` + More details: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically + minimum: 0 + maximum: 3600 + cleanup: + type: object + description: "Optional, defines behavior for cleanup Kubernetes resources during reconcile cycle" + # nullable: true + properties: + unknownObjects: + type: object + description: | + Describes what clickhouse-operator should do with found Kubernetes resources which should be managed by clickhouse-operator, + but do not have `ownerReference` to any currently managed `ClickHouseInstallation` resource. + Default behavior is `Delete`" + # nullable: true + properties: + statefulSet: + type: string + description: "Behavior policy for unknown StatefulSet, `Delete` by default" + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown PVC, `Delete` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown ConfigMap, `Delete` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown Service, `Delete` by default" + reconcileFailedObjects: + type: object + description: | + Describes what clickhouse-operator should do with Kubernetes resources which are failed during reconcile. + Default behavior is `Retain`" + # nullable: true + properties: + statefulSet: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed StatefulSet, `Retain` by default" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed PVC, `Retain` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed ConfigMap, `Retain` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed Service, `Retain` by default" + macros: + type: object + description: "macros parameters" + properties: + sections: + type: object + description: "sections behaviour for macros" + properties: + users: + type: object + description: "sections behaviour for macros on users" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + profiles: + type: object + description: "sections behaviour for macros on profiles" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + quotas: + type: object + description: "sections behaviour for macros on quotas" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + settings: + type: object + description: "sections behaviour for macros on settings" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + files: + type: object + description: "sections behaviour for macros on files" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + statefulSet: + type: object + description: "Optional, StatefulSet reconcile behavior tuning" + properties: + create: + type: object + description: "Behavior during create StatefulSet" + properties: + onFailure: + type: string + description: | + What to do in case created StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. delete - delete newly created problematic StatefulSet and follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "delete" + - "ignore" + update: + type: object + description: "Behavior during update StatefulSet" + properties: + timeout: + type: integer + description: "How many seconds to wait for StatefulSet to be 'Ready' during update" + minimum: 0 + maximum: 3600 + pollInterval: + type: integer + description: "How many seconds to wait between checks for StatefulSet status during update" + minimum: 1 + maximum: 600 + onFailure: + type: string + description: | + What to do in case updated StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. rollback - delete Pod and rollback StatefulSet to previous Generation. Follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "rollback" + - "ignore" + recreate: + type: object + description: "Behavior during recreate StatefulSet" + properties: + onDataLoss: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to PVC data loss or missing volumes. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + onUpdateFailure: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to update failure or StatefulSet not ready. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + reconcile: + type: object + # nullable: true + properties: + policy: + type: string + description: | + DISCUSSED TO BE DEPRECATED + Syntax sugar + Overrides all three 'reconcile.host.wait.{exclude, queries, include}' values from the operator's config + Possible values: + - wait - should wait to exclude host, complete queries and include host back into the cluster + - nowait - should NOT wait to exclude host, complete queries and include host back into the cluster + enum: + - "" + - "wait" + - "nowait" + configMapPropagationTimeout: + type: integer + description: | + Timeout in seconds for `clickhouse-operator` to wait for modified `ConfigMap` to propagate into the `Pod` + More details: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically + minimum: 0 + maximum: 3600 + cleanup: + type: object + description: "Optional, defines behavior for cleanup Kubernetes resources during reconcile cycle" + # nullable: true + properties: + unknownObjects: + type: object + description: | + Describes what clickhouse-operator should do with found Kubernetes resources which should be managed by clickhouse-operator, + but do not have `ownerReference` to any currently managed `ClickHouseInstallation` resource. + Default behavior is `Delete`" + # nullable: true + properties: + statefulSet: + type: string + description: "Behavior policy for unknown StatefulSet, `Delete` by default" + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown PVC, `Delete` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown ConfigMap, `Delete` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown Service, `Delete` by default" + reconcileFailedObjects: + type: object + description: | + Describes what clickhouse-operator should do with Kubernetes resources which are failed during reconcile. + Default behavior is `Retain`" + # nullable: true + properties: + statefulSet: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed StatefulSet, `Retain` by default" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed PVC, `Retain` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed ConfigMap, `Retain` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed Service, `Retain` by default" + macros: + type: object + description: "macros parameters" + properties: + sections: + type: object + description: "sections behaviour for macros" + properties: + users: + type: object + description: "sections behaviour for macros on users" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + profiles: + type: object + description: "sections behaviour for macros on profiles" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + quotas: + type: object + description: "sections behaviour for macros on quotas" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + settings: + type: object + description: "sections behaviour for macros on settings" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + files: + type: object + description: "sections behaviour for macros on files" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + statefulSet: + type: object + description: "Optional, StatefulSet reconcile behavior tuning" + properties: + create: + type: object + description: "Behavior during create StatefulSet" + properties: + onFailure: + type: string + description: | + What to do in case created StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. delete - delete newly created problematic StatefulSet and follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "delete" + - "ignore" + update: + type: object + description: "Behavior during update StatefulSet" + properties: + timeout: + type: integer + description: "How many seconds to wait for StatefulSet to be 'Ready' during update" + minimum: 0 + maximum: 3600 + pollInterval: + type: integer + description: "How many seconds to wait between checks for StatefulSet status during update" + minimum: 1 + maximum: 600 + onFailure: + type: string + description: | + What to do in case updated StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. rollback - delete Pod and rollback StatefulSet to previous Generation. Follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "rollback" + - "ignore" + recreate: + type: object + description: "Behavior during recreate StatefulSet" + properties: + onDataLoss: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to PVC data loss or missing volumes. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + onUpdateFailure: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to update failure or StatefulSet not ready. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + description: "Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + defaults: + type: object + description: | + define default behavior for whole ClickHouseInstallation, some behavior can be re-define on cluster, shard and replica level + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specdefaults + # nullable: true + properties: + replicasUseFQDN: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + define should replicas be specified by FQDN in `<host></host>`. + In case of "no" will use short hostname and clickhouse-server will use kubernetes default suffixes for DNS lookup + "no" by default + distributedDDL: + type: object + description: | + allows change `<yandex><distributed_ddl></distributed_ddl></yandex>` settings + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings-distributed_ddl + # nullable: true + properties: + profile: + type: string + description: "Settings from this profile will be used to execute DDL queries" + storageManagement: + type: object + description: default storage management options + properties: + provisioner: + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + templates: + type: object + description: "optional, configuration of the templates names which will use for generate Kubernetes resources according to one or more ClickHouse clusters described in current ClickHouseInstallation (chi) resource" + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + configuration: + type: object + description: "allows configure multiple aspects and behavior for `clickhouse-server` instance and also allows describe multiple `clickhouse-server` clusters inside one `chi` resource" + # nullable: true + properties: + zookeeper: + type: object + description: | + allows configure <yandex><zookeeper>..</zookeeper></yandex> section in each `Pod` during generate `ConfigMap` which will mounted in `/etc/clickhouse-server/config.d/` + `clickhouse-operator` itself doesn't manage Zookeeper, please install Zookeeper separatelly look examples on https://github.com/Altinity/clickhouse-operator/tree/master/deploy/zookeeper/ + currently, zookeeper (or clickhouse-keeper replacement) used for *ReplicatedMergeTree table engines and for `distributed_ddl` + More details: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings_zookeeper + # nullable: true + properties: + nodes: + type: array + description: "describe every available zookeeper cluster node for interaction" + # nullable: true + items: + type: object + #required: + # - host + properties: + host: + type: string + description: "dns name or ip address for Zookeeper node" + port: + type: integer + description: "TCP port which used to connect to Zookeeper node" + minimum: 0 + maximum: 65535 + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "if a secure connection to Zookeeper is required" + availabilityZone: + type: string + description: "availability zone for Zookeeper node" + session_timeout_ms: + type: integer + description: "session timeout during connect to Zookeeper" + operation_timeout_ms: + type: integer + description: "one operation timeout during Zookeeper transactions" + root: + type: string + description: "optional root znode path inside zookeeper to store ClickHouse related data (replication queue or distributed DDL)" + identity: + type: string + description: "optional access credentials string with `user:password` format used when use digest authorization in Zookeeper" + use_compression: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Enables compression in Keeper protocol if set to true" + users: + type: object + description: | + allows configure <yandex><users>..</users></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure password hashed, authorization restrictions, database level security row filters etc. + More details: https://clickhouse.tech/docs/en/operations/settings/settings-users/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationusers + + any key could contains `valueFrom` with `secretKeyRef` which allow pass password from kubernetes secrets + secret value will pass in `pod.spec.containers.evn`, and generate with from_env=XXX in XML in /etc/clickhouse-server/users.d/chop-generated-users.xml + it not allow automatically updates when updates `secret`, change spec.taskID for manually trigger reconcile cycle + + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + any key with prefix `k8s_secret_` shall has value with format namespace/secret/key or secret/key + in this case value from secret will write directly into XML tag during render *-usersd ConfigMap + + any key with prefix `k8s_secret_env` shall has value with format namespace/secret/key or secret/key + in this case value from secret will write into environment variable and write to XML tag via from_env=XXX + + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + # nullable: true + x-kubernetes-preserve-unknown-fields: true + profiles: + type: object + description: | + allows configure <yandex><profiles>..</profiles></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure any aspect of settings profile + More details: https://clickhouse.tech/docs/en/operations/settings/settings-profiles/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationprofiles + # nullable: true + x-kubernetes-preserve-unknown-fields: true + quotas: + type: object + description: | + allows configure <yandex><quotas>..</quotas></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure any aspect of resource quotas + More details: https://clickhouse.tech/docs/en/operations/quotas/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationquotas + # nullable: true + x-kubernetes-preserve-unknown-fields: true + settings: + type: object + description: | + allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationsettings + + any key could contains `valueFrom` with `secretKeyRef` which allow pass password from kubernetes secrets + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + secret value will pass in `pod.spec.env`, and generate with from_env=XXX in XML in /etc/clickhouse-server/config.d/chop-generated-settings.xml + it not allow automatically updates when updates `secret`, change spec.taskID for manually trigger reconcile cycle + # nullable: true + x-kubernetes-preserve-unknown-fields: true + files: + type: object + description: | + allows define content of any setting file inside each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + every key in this object is the file name + every value in this object is the file content + you can use `!!binary |` and base64 for binary files, see details here https://yaml.org/type/binary.html + each key could contains prefix like {common}, {users}, {hosts} or config.d, users.d, conf.d, wrong prefixes will be ignored, subfolders also will be ignored + More details: https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-05-files-nested.yaml + + any key could contains `valueFrom` with `secretKeyRef` which allow pass values from kubernetes secrets + secrets will mounted into pod as separate volume in /etc/clickhouse-server/secrets.d/ + and will automatically update when update secret + it useful for pass SSL certificates from cert-manager or similar tool + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + # nullable: true + x-kubernetes-preserve-unknown-fields: true + clusters: + type: array + description: | + describes clusters layout and allows change settings on cluster-level, shard-level and replica-level + every cluster is a set of StatefulSet, one StatefulSet contains only one Pod with `clickhouse-server` + all Pods will rendered in <remote_server> part of ClickHouse configs, mounted from ConfigMap as `/etc/clickhouse-server/config.d/chop-generated-remote_servers.xml` + Clusters will use for Distributed table engine, more details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + If `cluster` contains zookeeper settings (could be inherited from top `chi` level), when you can create *ReplicatedMergeTree tables + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "cluster name, used to identify set of servers and wide used during generate names of related Kubernetes resources" + minLength: 1 + # See namePartClusterMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + zookeeper: + type: object + # nullable: true + properties: + nodes: + type: array + description: "describe every available zookeeper cluster node for interaction" + # nullable: true + items: + type: object + #required: + # - host + properties: + host: + type: string + description: "dns name or ip address for Zookeeper node" + port: + type: integer + description: "TCP port which used to connect to Zookeeper node" + minimum: 0 + maximum: 65535 + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "if a secure connection to Zookeeper is required" + availabilityZone: + type: string + description: "availability zone for Zookeeper node" + session_timeout_ms: + type: integer + description: "session timeout during connect to Zookeeper" + operation_timeout_ms: + type: integer + description: "one operation timeout during Zookeeper transactions" + root: + type: string + description: "optional root znode path inside zookeeper to store ClickHouse related data (replication queue or distributed DDL)" + identity: + type: string + description: "optional access credentials string with `user:password` format used when use digest authorization in Zookeeper" + use_compression: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Enables compression in Keeper protocol if set to true" + description: | + optional, allows configure <yandex><zookeeper>..</zookeeper></yandex> section in each `Pod` only in current ClickHouse cluster, during generate `ConfigMap` which will mounted in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.zookeeper` settings + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` on current cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected cluster + override top-level `chi.spec.configuration.templates` + schemaPolicy: + type: object + description: | + describes how schema is propagated within replicas and shards + properties: + replica: + type: string + description: "how schema is propagated within a replica" + enum: + # List SchemaPolicyReplicaXXX constants from model + - "" + - "None" + - "All" + shard: + type: string + description: "how schema is propagated between shards" + enum: + # List SchemaPolicyShardXXX constants from model + - "" + - "None" + - "All" + - "DistributedTablesOnly" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: optional, open secure ports for cluster + secret: + type: object + description: "optional, shared secret value to secure cluster communications" + properties: + auto: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Auto-generate shared secret value to secure cluster communications" + value: + description: "Cluster shared secret value in plain text" + type: string + valueFrom: + description: "Cluster shared secret source" + type: object + properties: + secretKeyRef: + description: | + Selects a key of a secret in the clickhouse installation namespace. + Should not be used if value is not empty. + type: object + properties: + name: + description: | + Name of the referent. More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + key: + description: The key of the secret to select from. Must be a valid secret key. + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - name + - key + pdbManaged: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Specifies whether the Pod Disruption Budget (PDB) should be managed. + During the next installation, if PDB management is enabled, the operator will + attempt to retrieve any existing PDB. If none is found, it will create a new one + and initiate a reconciliation loop. If PDB management is disabled, the existing PDB + will remain intact, and the reconciliation loop will not be executed. By default, + PDB management is enabled. + pdbMaxUnavailable: + type: integer + description: | + Pod eviction is allowed if at most "pdbMaxUnavailable" pods are unavailable after the eviction, + i.e. even in absence of the evicted pod. For example, one can prevent all voluntary evictions + by specifying 0. This is a mutually exclusive setting with "minAvailable". + minimum: 0 + maximum: 65535 + reconcile: + type: object + description: "allow tuning reconciling process" + properties: + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + layout: + type: object + description: | + describe current cluster layout, how much shards in cluster, how much replica in shard + allows override settings on each shard and replica separatelly + # nullable: true + properties: + shardsCount: + type: integer + description: | + how much shards for current ClickHouse cluster will run in Kubernetes, + each shard contains shared-nothing part of data and contains set of replicas, + cluster contains 1 shard by default" + replicasCount: + type: integer + description: | + how much replicas in each shards for current cluster will run in Kubernetes, + each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + every shard contains 1 replica by default" + shards: + type: array + description: | + optional, allows override top-level `chi.spec.configuration`, cluster-level + `chi.spec.configuration.clusters` settings for each shard separately, + use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + definitionType: + type: string + description: "DEPRECATED - to be removed soon" + weight: + type: integer + description: | + optional, 1 by default, allows setup shard <weight> setting which will use during insert into tables with `Distributed` engine, + will apply in <remote_servers> inside ConfigMap which will mount in /etc/clickhouse-server/config.d/chop-generated-remote_servers.xml + More details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + internalReplication: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, `true` by default when `chi.spec.configuration.clusters[].layout.ReplicaCount` > 1 and 0 otherwise + allows setup <internal_replication> setting which will use during insert into tables with `Distributed` engine for insert only in one live replica and other replicas will download inserted data during replication, + will apply in <remote_servers> inside ConfigMap which will mount in /etc/clickhouse-server/config.d/chop-generated-remote_servers.xml + More details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one shard during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` and cluster-level `chi.spec.configuration.clusters.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected shard + override top-level `chi.spec.configuration.templates` and cluster-level `chi.spec.configuration.clusters.templates` + replicasCount: + type: integer + description: | + optional, how much replicas in selected shard for selected ClickHouse cluster will run in Kubernetes, each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + shard contains 1 replica by default + override cluster-level `chi.spec.configuration.clusters.layout.replicasCount` + minimum: 1 + replicas: + type: array + description: | + optional, allows override behavior for selected replicas from cluster-level `chi.spec.configuration.clusters` and shard-level `chi.spec.configuration.clusters.layout.shards` + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `tcp` for selected replica, override `chi.spec.templates.hostTemplates.spec.tcpPort` + allows connect to `clickhouse-server` via TCP Native protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `http` for selected replica, override `chi.spec.templates.hostTemplates.spec.httpPort` + allows connect to `clickhouse-server` via HTTP protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `interserver` for selected replica, override `chi.spec.templates.hostTemplates.spec.interserverHTTPPort` + allows connect between replicas inside same shard during fetch replicated data parts HTTP protocol + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and shard-level `chi.spec.configuration.clusters.layout.shards.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files`, cluster-level `chi.spec.configuration.clusters.files` and shard-level `chi.spec.configuration.clusters.layout.shards.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` and shard-level `chi.spec.configuration.clusters.layout.shards.templates` + replicas: + type: array + description: "optional, allows override top-level `chi.spec.configuration` and cluster-level `chi.spec.configuration.clusters` configuration for each replica and each shard relates to selected replica, use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and will ignore if shard-level `chi.spec.configuration.clusters.layout.shards` present + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` + shardsCount: + type: integer + description: "optional, count of shards related to current replica, you can override each shard behavior on low-level `chi.spec.configuration.clusters.layout.replicas.shards`" + minimum: 1 + shards: + type: array + description: "optional, list of shards related to current replica, will ignore if `chi.spec.configuration.clusters.layout.shards` presents" + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `tcp` for selected shard, override `chi.spec.templates.hostTemplates.spec.tcpPort` + allows connect to `clickhouse-server` via TCP Native protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `http` for selected shard, override `chi.spec.templates.hostTemplates.spec.httpPort` + allows connect to `clickhouse-server` via HTTP protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `interserver` for selected shard, override `chi.spec.templates.hostTemplates.spec.interserverHTTPPort` + allows connect between replicas inside same shard during fetch replicated data parts HTTP protocol + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and replica-level `chi.spec.configuration.clusters.layout.replicas.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates` + templates: + type: object + description: "allows define templates which will use for render Kubernetes resources like StatefulSet, ConfigMap, Service, PVC, by default, clickhouse-operator have own templates, but you can override it" + # nullable: true + properties: + hostTemplates: + type: array + description: "hostTemplate will use during apply to generate `clickhose-server` config files" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.hostTemplate`, cluster-level `chi.spec.configuration.clusters.templates.hostTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.hostTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.hostTemplate`" + type: string + portDistribution: + type: array + description: "define how will distribute numeric values of named ports in `Pod.spec.containers.ports` and clickhouse-server configs" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "type of distribution, when `Unspecified` (default value) then all listen ports on clickhouse-server configuration in all Pods will have the same value, when `ClusterScopeIndex` then ports will increment to offset from base value depends on shard and replica index inside cluster with combination of `chi.spec.templates.podTemlates.spec.HostNetwork` it allows setup ClickHouse cluster inside Kubernetes and provide access via external network bypass Kubernetes internal network" + enum: + # List PortDistributionXXX constants + - "" + - "Unspecified" + - "ClusterScopeIndex" + spec: + # Host + type: object + properties: + name: + type: string + description: "by default, hostname will generate, but this allows define custom name for each `clickhouse-server`" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `tcp_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=tcp]` + More info: https://clickhouse.tech/docs/en/interfaces/tcp/ + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `http_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=http]` + More info: https://clickhouse.tech/docs/en/interfaces/http/ + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `interserver_http_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=interserver]` + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#interserver-http-port + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: "be careful, this part of CRD allows override template inside template, don't use it if you don't understand what you do" + podTemplates: + type: array + description: | + podTemplate will use during render `Pod` inside `StatefulSet.spec` and allows define rendered `Pod.spec`, pod scheduling distribution and pod zone + More information: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatespodtemplates + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.podTemplate`, cluster-level `chi.spec.configuration.clusters.templates.podTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.podTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.podTemplate`" + generateName: + type: string + description: "allows define format for generated `Pod` name, look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates for details about available template variables" + zone: + type: object + description: "allows define custom zone name and will separate ClickHouse `Pods` between nodes, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + #required: + # - values + properties: + key: + type: string + description: "optional, if defined, allows select kubernetes nodes by label with `name` equal `key`" + values: + type: array + description: "optional, if defined, allows select kubernetes nodes by label with `value` in `values`" + # nullable: true + items: + type: string + distribution: + type: string + description: "DEPRECATED, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + enum: + - "" + - "Unspecified" + - "OnePerHost" + podDistribution: + type: array + description: "define ClickHouse Pod distribution policy between Kubernetes Nodes inside Shard, Replica, Namespace, CHI, another ClickHouse cluster" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "you can define multiple affinity policy types" + enum: + # List PodDistributionXXX constants + - "" + - "Unspecified" + - "ClickHouseAntiAffinity" + - "ShardAntiAffinity" + - "ReplicaAntiAffinity" + - "AnotherNamespaceAntiAffinity" + - "AnotherClickHouseInstallationAntiAffinity" + - "AnotherClusterAntiAffinity" + - "MaxNumberPerNode" + - "NamespaceAffinity" + - "ClickHouseInstallationAffinity" + - "ClusterAffinity" + - "ShardAffinity" + - "ReplicaAffinity" + - "PreviousTailAffinity" + - "CircularReplication" + scope: + type: string + description: "scope for apply each podDistribution" + enum: + # list PodDistributionScopeXXX constants + - "" + - "Unspecified" + - "Shard" + - "Replica" + - "Cluster" + - "ClickHouseInstallation" + - "Namespace" + number: + type: integer + description: "define, how much ClickHouse Pods could be inside selected scope with selected distribution type" + minimum: 0 + maximum: 65535 + topologyKey: + type: string + description: | + use for inter-pod affinity look to `pod.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution.podAffinityTerm.topologyKey`, + more info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity" + metadata: + type: object + description: | + allows pass standard object's metadata from template to Pod + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify PodSpec + type: object + description: "allows define whole Pod.spec inside StaefulSet.spec, look to https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates for details" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + volumeClaimTemplates: + type: array + description: | + allows define template for rendering `PVC` kubernetes resource, which would use inside `Pod` for mount clickhouse `data`, clickhouse `logs` or something else + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + top-level `chi.spec.defaults.templates.dataVolumeClaimTemplate` or `chi.spec.defaults.templates.logVolumeClaimTemplate`, + cluster-level `chi.spec.configuration.clusters.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.templates.logVolumeClaimTemplate`, + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.shards.temlates.logVolumeClaimTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.replicas.templates.logVolumeClaimTemplate` + provisioner: + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + metadata: + type: object + description: | + allows to pass standard object's metadata from template to PVC + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + description: | + allows define all aspects of `PVC` resource + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims + # nullable: true + x-kubernetes-preserve-unknown-fields: true + serviceTemplates: + type: array + description: | + allows define template for rendering `Service` which would get endpoint from Pods which scoped chi-wide, cluster-wide, shard-wide, replica-wide level + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + chi-level `chi.spec.defaults.templates.serviceTemplate` + cluster-level `chi.spec.configuration.clusters.templates.clusterServiceTemplate` + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.shardServiceTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.replicaServiceTemplate` or `chi.spec.configuration.clusters.layout.shards.replicas.replicaServiceTemplate` + generateName: + type: string + description: | + allows define format for generated `Service` name, + look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates + for details about available template variables" + metadata: + # TODO specify ObjectMeta + type: object + description: | + allows pass standard object's metadata from template to Service + Could be use for define specificly for Cloud Provider metadata which impact to behavior of service + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify ServiceSpec + type: object + description: | + describe behavior of generated Service + More info: https://kubernetes.io/docs/concepts/services-networking/service/ + # nullable: true + x-kubernetes-preserve-unknown-fields: true + useTemplates: + type: array + description: | + list of `ClickHouseInstallationTemplate` (chit) resource names which will merge with current `CHI` + manifest during render Kubernetes resources to create related ClickHouse clusters" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "name of `ClickHouseInstallationTemplate` (chit) resource" + namespace: + type: string + description: "Kubernetes namespace where need search `chit` resource, depending on `watchNamespaces` settings in `clickhouse-operator`" + useType: + type: string + description: "optional, current strategy is only merge, and current `chi` settings have more priority than merged template `chit`" + enum: + # List useTypeXXX constants from model + - "" + - "merge" +--- +# Template Parameters: +# +# NONE +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clickhouseoperatorconfigurations.clickhouse.altinity.com + labels: + clickhouse.altinity.com/chop: 0.26.3 +spec: + group: clickhouse.altinity.com + scope: Namespaced + names: + kind: ClickHouseOperatorConfiguration + singular: clickhouseoperatorconfiguration + plural: clickhouseoperatorconfigurations + shortNames: + - chopconf + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: namespaces + type: string + description: Watch namespaces + jsonPath: .status + - name: age + type: date + description: Age of the resource + # Displayed in all priorities + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + description: "allows customize `clickhouse-operator` settings, need restart clickhouse-operator pod after adding, more details https://github.com/Altinity/clickhouse-operator/blob/master/docs/operator_configuration.md" + x-kubernetes-preserve-unknown-fields: true + properties: + status: + type: object + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + description: | + Allows to define settings of the clickhouse-operator. + More info: https://github.com/Altinity/clickhouse-operator/blob/master/config/config.yaml + Check into etc-clickhouse-operator* ConfigMaps if you need more control + x-kubernetes-preserve-unknown-fields: true + properties: + watch: + type: object + description: "Parameters for watch kubernetes resources which used by clickhouse-operator deployment" + properties: + namespaces: + type: object + description: "List of namespaces where clickhouse-operator watches for events." + x-kubernetes-preserve-unknown-fields: true + clickhouse: + type: object + description: "Clickhouse related parameters used by clickhouse-operator" + properties: + configuration: + type: object + properties: + file: + type: object + properties: + path: + type: object + description: | + Each 'path' can be either absolute or relative. + In case path is absolute - it is used as is. + In case path is relative - it is relative to the folder where configuration file you are reading right now is located. + properties: + common: + type: string + description: | + Path to the folder where ClickHouse configuration files common for all instances within a CHI are located. + Default value - config.d + host: + type: string + description: | + Path to the folder where ClickHouse configuration files unique for each instance (host) within a CHI are located. + Default value - conf.d + user: + type: string + description: | + Path to the folder where ClickHouse configuration files with users settings are located. + Files are common for all instances within a CHI. + Default value - users.d + user: + type: object + description: "Default parameters for any user which will create" + properties: + default: + type: object + properties: + profile: + type: string + description: "ClickHouse server configuration `<profile>...</profile>` for any <user>" + quota: + type: string + description: "ClickHouse server configuration `<quota>...</quota>` for any <user>" + networksIP: + type: array + description: "ClickHouse server configuration `<networks><ip>...</ip></networks>` for any <user>" + items: + type: string + password: + type: string + description: "ClickHouse server configuration `<password>...</password>` for any <user>" + network: + type: object + description: "Default network parameters for any user which will create" + properties: + hostRegexpTemplate: + type: string + description: "ClickHouse server configuration `<host_regexp>...</host_regexp>` for any <user>" + configurationRestartPolicy: + type: object + description: "Configuration restart policy describes what configuration changes require ClickHouse restart" + properties: + rules: + type: array + description: "Array of set of rules per specified ClickHouse versions" + items: + type: object + properties: + version: + type: string + description: "ClickHouse version expression" + rules: + type: array + description: "Set of configuration rules for specified ClickHouse version" + items: + type: object + description: "setting: value pairs for configuration restart policy" + x-kubernetes-preserve-unknown-fields: true + access: + type: object + description: "parameters which use for connect to clickhouse from clickhouse-operator deployment" + properties: + scheme: + type: string + description: "The scheme to user for connecting to ClickHouse. Possible values: http, https, auto" + username: + type: string + description: "ClickHouse username to be used by operator to connect to ClickHouse instances, deprecated, use chCredentialsSecretName" + password: + type: string + description: "ClickHouse password to be used by operator to connect to ClickHouse instances, deprecated, use chCredentialsSecretName" + rootCA: + type: string + description: "Root certificate authority that clients use when verifying server certificates. Used for https connection to ClickHouse" + secret: + type: object + properties: + namespace: + type: string + description: "Location of k8s Secret with username and password to be used by operator to connect to ClickHouse instances" + name: + type: string + description: "Name of k8s Secret with username and password to be used by operator to connect to ClickHouse instances" + port: + type: integer + minimum: 1 + maximum: 65535 + description: "Port to be used by operator to connect to ClickHouse instances" + timeouts: + type: object + description: "Timeouts used to limit connection and queries from the operator to ClickHouse instances, In seconds" + properties: + connect: + type: integer + minimum: 1 + maximum: 10 + description: "Timout to setup connection from the operator to ClickHouse instances. In seconds." + query: + type: integer + minimum: 1 + maximum: 600 + description: "Timout to perform SQL query from the operator to ClickHouse instances. In seconds." + addons: + type: object + description: "Configuration addons specifies additional settings" + properties: + rules: + type: array + description: "Array of set of rules per specified ClickHouse versions" + items: + type: object + properties: + version: + type: string + description: "ClickHouse version expression" + spec: + type: object + description: "spec" + properties: + configuration: + type: object + description: "allows configure multiple aspects and behavior for `clickhouse-server` instance and also allows describe multiple `clickhouse-server` clusters inside one `chi` resource" + properties: + users: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + profiles: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + quotas: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + settings: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + files: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + metrics: + type: object + description: "parameters which use for connect to fetch metrics from clickhouse by clickhouse-operator" + properties: + timeouts: + type: object + description: | + Timeouts used to limit connection and queries from the metrics exporter to ClickHouse instances + Specified in seconds. + properties: + collect: + type: integer + minimum: 1 + maximum: 600 + description: | + Timeout used to limit metrics collection request. In seconds. + Upon reaching this timeout metrics collection is aborted and no more metrics are collected in this cycle. + All collected metrics are returned. + tablesRegexp: + type: string + description: | + Regexp to match tables in system database to fetch metrics from. + Multiple tables can be matched using regexp. Matched tables are merged using merge() table function. + Default is "^(metrics|custom_metrics)$". + template: + type: object + description: "Parameters which are used if you want to generate ClickHouseInstallationTemplate custom resources from files which are stored inside clickhouse-operator deployment" + properties: + chi: + type: object + properties: + policy: + type: string + description: | + CHI template updates handling policy + Possible policy values: + - ReadOnStart. Accept CHIT updates on the operators start only. + - ApplyOnNextReconcile. Accept CHIT updates at all time. Apply news CHITs on next regular reconcile of the CHI + enum: + - "" + - "ReadOnStart" + - "ApplyOnNextReconcile" + path: + type: string + description: "Path to folder where ClickHouseInstallationTemplate .yaml manifests are located." + reconcile: + type: object + description: "allow tuning reconciling process" + properties: + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileCHIsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "How many goroutines will be used to reconcile CHIs in parallel, 10 by default" + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "How many goroutines will be used to reconcile shards of a cluster in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + statefulSet: + type: object + description: "Allow change default behavior for reconciling StatefulSet which generated by clickhouse-operator" + properties: + create: + type: object + description: "Behavior during create StatefulSet" + properties: + onFailure: + type: string + description: | + What to do in case created StatefulSet is not in Ready after `statefulSetUpdateTimeout` seconds + Possible options: + 1. abort - do nothing, just break the process and wait for admin. + 2. delete - delete newly created problematic StatefulSet. + 3. ignore (default) - ignore error, pretend nothing happened and move on to the next StatefulSet. + update: + type: object + description: "Behavior during update StatefulSet" + properties: + timeout: + type: integer + description: "How many seconds to wait for created/updated StatefulSet to be Ready" + pollInterval: + type: integer + description: "How many seconds to wait between checks for created/updated StatefulSet status" + onFailure: + type: string + description: | + What to do in case updated StatefulSet is not in Ready after `statefulSetUpdateTimeout` seconds + Possible options: + 1. abort - do nothing, just break the process and wait for admin. + 2. rollback (default) - delete Pod and rollback StatefulSet to previous Generation. Pod would be recreated by StatefulSet based on rollback-ed configuration. + 3. ignore - ignore error, pretend nothing happened and move on to the next StatefulSet. + recreate: + type: object + description: "Behavior during recreate StatefulSet" + properties: + onDataLoss: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to PVC data loss or missing volumes. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate (default) - proceed and recreate StatefulSet. + onUpdateFailure: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to update failure or StatefulSet not ready. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate (default) - proceed and recreate StatefulSet. + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be excluded from a ClickHouse cluster" + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for readiness probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + annotation: + type: object + description: "defines which metadata.annotations items will include or exclude during render StatefulSet, Pod, PVC resources" + properties: + include: + type: array + description: | + When propagating labels from the chi's `metadata.annotations` section to child objects' `metadata.annotations`, + include annotations with names from the following list + items: + type: string + exclude: + type: array + description: | + When propagating labels from the chi's `metadata.annotations` section to child objects' `metadata.annotations`, + exclude annotations with names from the following list + items: + type: string + label: + type: object + description: "defines which metadata.labels will include or exclude during render StatefulSet, Pod, PVC resources" + properties: + include: + type: array + description: | + When propagating labels from the chi's `metadata.labels` section to child objects' `metadata.labels`, + include labels from the following list + items: + type: string + exclude: + type: array + items: + type: string + description: | + When propagating labels from the chi's `metadata.labels` section to child objects' `metadata.labels`, + exclude labels from the following list + appendScope: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether to append *Scope* labels to StatefulSet and Pod + - "LabelShardScopeIndex" + - "LabelReplicaScopeIndex" + - "LabelCHIScopeIndex" + - "LabelCHIScopeCycleSize" + - "LabelCHIScopeCycleIndex" + - "LabelCHIScopeCycleOffset" + - "LabelClusterScopeIndex" + - "LabelClusterScopeCycleSize" + - "LabelClusterScopeCycleIndex" + - "LabelClusterScopeCycleOffset" + metrics: + type: object + description: "defines metrics exporter options" + properties: + labels: + type: object + description: "defines metric labels options" + properties: + exclude: + type: array + description: | + When adding labels to a metric exclude labels with names from the following list + items: + type: string + status: + type: object + description: "defines status options" + properties: + fields: + type: object + description: "defines status fields options" + properties: + action: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator should fill status field 'action'" + actions: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator should fill status field 'actions'" + error: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator should fill status field 'error'" + errors: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator should fill status field 'errors'" + statefulSet: + type: object + description: "define StatefulSet-specific parameters" + properties: + revisionHistoryLimit: + type: integer + description: "revisionHistoryLimit is the maximum number of revisions that will be\nmaintained in the StatefulSet's revision history. \nLook details in `statefulset.spec.revisionHistoryLimit`\n" + pod: + type: object + description: "define pod specific parameters" + properties: + terminationGracePeriod: + type: integer + description: "Optional duration in seconds the pod needs to terminate gracefully. \nLook details in `pod.spec.terminationGracePeriodSeconds`\n" + logger: + type: object + description: "allow setup clickhouse-operator logger behavior" + properties: + logtostderr: + type: string + description: "boolean, allows logs to stderr" + alsologtostderr: + type: string + description: "boolean allows logs to stderr and files both" + v: + type: string + description: "verbosity level of clickhouse-operator log, default - 1 max - 9" + stderrthreshold: + type: string + vmodule: + type: string + description: | + Comma-separated list of filename=N, where filename (can be a pattern) must have no .go ext, and N is a V level. + Ex.: file*=2 sets the 'V' to 2 in all files with names like file*. + log_backtrace_at: + type: string + description: | + It can be set to a file and line number with a logging line. + Ex.: file.go:123 + Each time when this line is being executed, a stack trace will be written to the Info log. +--- +# Template Parameters: +# +# OPERATOR_VERSION=0.26.3 +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clickhousekeeperinstallations.clickhouse-keeper.altinity.com + labels: + clickhouse-keeper.altinity.com/chop: 0.26.3 +spec: + group: clickhouse-keeper.altinity.com + scope: Namespaced + names: + kind: ClickHouseKeeperInstallation + singular: clickhousekeeperinstallation + plural: clickhousekeeperinstallations + shortNames: + - chk + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: status + type: string + description: Resource status + jsonPath: .status.status + - name: version + type: string + description: Operator version + priority: 1 # show in wide view + jsonPath: .status.chop-version + - name: clusters + type: integer + description: Clusters count + jsonPath: .status.clusters + - name: shards + type: integer + description: Shards count + priority: 1 # show in wide view + jsonPath: .status.shards + - name: hosts + type: integer + description: Hosts count + jsonPath: .status.hosts + - name: taskID + type: string + description: TaskID + priority: 1 # show in wide view + jsonPath: .status.taskID + - name: hosts-completed + type: integer + description: Completed hosts count + jsonPath: .status.hostsCompleted + - name: hosts-updated + type: integer + description: Updated hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsUpdated + - name: hosts-added + type: integer + description: Added hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsAdded + - name: hosts-deleted + type: integer + description: Hosts deleted count + priority: 1 # show in wide view + jsonPath: .status.hostsDeleted + - name: endpoint + type: string + description: Client access endpoint + priority: 1 # show in wide view + jsonPath: .status.endpoint + - name: age + type: date + description: Age of the resource + # Displayed in all priorities + jsonPath: .metadata.creationTimestamp + - name: suspend + type: string + description: Suspend reconciliation + # Displayed in all priorities + jsonPath: .spec.suspend + subresources: + status: {} + schema: + openAPIV3Schema: + description: "define a set of Kubernetes resources (StatefulSet, PVC, Service, ConfigMap) which describe behavior one or more clusters" + type: object + required: + - spec + properties: + apiVersion: + description: | + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: | + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + type: object + description: | + Status contains many fields like a normalized configuration, clickhouse-operator version, current action and all applied action list, current taskID and all applied taskIDs and other + properties: + chop-version: + type: string + description: "Operator version" + chop-commit: + type: string + description: "Operator git commit SHA" + chop-date: + type: string + description: "Operator build date" + chop-ip: + type: string + description: "IP address of the operator's pod which managed this resource" + clusters: + type: integer + minimum: 0 + description: "Clusters count" + shards: + type: integer + minimum: 0 + description: "Shards count" + replicas: + type: integer + minimum: 0 + description: "Replicas count" + hosts: + type: integer + minimum: 0 + description: "Hosts count" + status: + type: string + description: "Status" + taskID: + type: string + description: "Current task id" + taskIDsStarted: + type: array + description: "Started task ids" + nullable: true + items: + type: string + taskIDsCompleted: + type: array + description: "Completed task ids" + nullable: true + items: + type: string + action: + type: string + description: "Action" + actions: + type: array + description: "Actions" + nullable: true + items: + type: string + error: + type: string + description: "Last error" + errors: + type: array + description: "Errors" + nullable: true + items: + type: string + hostsUnchanged: + type: integer + minimum: 0 + description: "Unchanged Hosts count" + hostsUpdated: + type: integer + minimum: 0 + description: "Updated Hosts count" + hostsAdded: + type: integer + minimum: 0 + description: "Added Hosts count" + hostsCompleted: + type: integer + minimum: 0 + description: "Completed Hosts count" + hostsDeleted: + type: integer + minimum: 0 + description: "Deleted Hosts count" + hostsDelete: + type: integer + minimum: 0 + description: "About to delete Hosts count" + pods: + type: array + description: "Pods" + nullable: true + items: + type: string + pod-ips: + type: array + description: "Pod IPs" + nullable: true + items: + type: string + fqdns: + type: array + description: "Pods FQDNs" + nullable: true + items: + type: string + endpoint: + type: string + description: "Endpoint" + endpoints: + type: array + description: "All endpoints" + nullable: true + items: + type: string + generation: + type: integer + minimum: 0 + description: "Generation" + normalized: + type: object + description: "Normalized resource requested" + nullable: true + x-kubernetes-preserve-unknown-fields: true + normalizedCompleted: + type: object + description: "Normalized resource completed" + nullable: true + x-kubernetes-preserve-unknown-fields: true + hostsWithTablesCreated: + type: array + description: "List of hosts with tables created by the operator" + nullable: true + items: + type: string + hostsWithReplicaCaughtUp: + type: array + description: "List of hosts with replica caught up" + nullable: true + items: + type: string + usedTemplates: + type: array + description: "List of templates used to build this CHI" + nullable: true + x-kubernetes-preserve-unknown-fields: true + items: + type: object + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + # x-kubernetes-preserve-unknown-fields: true + description: | + Specification of the desired behavior of one or more ClickHouse clusters + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md + properties: + taskID: + type: string + description: | + Allows to define custom taskID for CHI update and watch status of this update execution. + Displayed in all .status.taskID* fields. + By default (if not filled) every update of CHI manifest will generate random taskID + stop: + type: string + description: | + Allows to stop all ClickHouse Keeper clusters defined in a CHK. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + suspend: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Suspend reconciliation of resources managed by a ClickHouse Keeper. + Works as the following: + - When `suspend` is `true` operator stops reconciling all resources. + - When `suspend` is `false` or not set, operator reconciles all resources. + namespaceDomainPattern: + type: string + description: | + Custom domain pattern which will be used for DNS names of `Service` or `Pod`. + Typical use scenario - custom cluster domain in Kubernetes cluster + Example: %s.svc.my.test + reconciling: + type: object + description: "Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + # nullable: true + properties: + policy: + type: string + description: | + DISCUSSED TO BE DEPRECATED + Syntax sugar + Overrides all three 'reconcile.host.wait.{exclude, queries, include}' values from the operator's config + Possible values: + - wait - should wait to exclude host, complete queries and include host back into the cluster + - nowait - should NOT wait to exclude host, complete queries and include host back into the cluster + enum: + - "" + - "wait" + - "nowait" + configMapPropagationTimeout: + type: integer + description: | + Timeout in seconds for `clickhouse-operator` to wait for modified `ConfigMap` to propagate into the `Pod` + More details: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically + minimum: 0 + maximum: 3600 + cleanup: + type: object + description: "Optional, defines behavior for cleanup Kubernetes resources during reconcile cycle" + # nullable: true + properties: + unknownObjects: + type: object + description: | + Describes what clickhouse-operator should do with found Kubernetes resources which should be managed by clickhouse-operator, + but do not have `ownerReference` to any currently managed `ClickHouseInstallation` resource. + Default behavior is `Delete`" + # nullable: true + properties: + statefulSet: + type: string + description: "Behavior policy for unknown StatefulSet, `Delete` by default" + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown PVC, `Delete` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown ConfigMap, `Delete` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown Service, `Delete` by default" + reconcileFailedObjects: + type: object + description: | + Describes what clickhouse-operator should do with Kubernetes resources which are failed during reconcile. + Default behavior is `Retain`" + # nullable: true + properties: + statefulSet: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed StatefulSet, `Retain` by default" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed PVC, `Retain` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed ConfigMap, `Retain` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed Service, `Retain` by default" + defaults: + type: object + description: | + define default behavior for whole ClickHouseInstallation, some behavior can be re-define on cluster, shard and replica level + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specdefaults + # nullable: true + properties: + replicasUseFQDN: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + define should replicas be specified by FQDN in `<host></host>`. + In case of "no" will use short hostname and clickhouse-server will use kubernetes default suffixes for DNS lookup + "no" by default + distributedDDL: + type: object + description: | + allows change `<yandex><distributed_ddl></distributed_ddl></yandex>` settings + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings-distributed_ddl + # nullable: true + properties: + profile: + type: string + description: "Settings from this profile will be used to execute DDL queries" + storageManagement: + type: object + description: default storage management options + properties: + provisioner: + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + templates: + type: object + description: "optional, configuration of the templates names which will use for generate Kubernetes resources according to one or more ClickHouse clusters described in current ClickHouseInstallation (chi) resource" + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + configuration: + type: object + description: "allows configure multiple aspects and behavior for `clickhouse-server` instance and also allows describe multiple `clickhouse-server` clusters inside one `chi` resource" + # nullable: true + properties: + settings: + type: object + description: | + allows configure multiple aspects and behavior for `clickhouse-keeper` instance + # nullable: true + x-kubernetes-preserve-unknown-fields: true + files: + type: object + description: | + allows define content of any setting + # nullable: true + x-kubernetes-preserve-unknown-fields: true + clusters: + type: array + description: | + describes clusters layout and allows change settings on cluster-level and replica-level + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "cluster name, used to identify set of servers and wide used during generate names of related Kubernetes resources" + minLength: 1 + # See namePartClusterMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` on current cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected cluster + override top-level `chi.spec.configuration.templates` + pdbManaged: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Specifies whether the Pod Disruption Budget (PDB) should be managed. + During the next installation, if PDB management is enabled, the operator will + attempt to retrieve any existing PDB. If none is found, it will create a new one + and initiate a reconciliation loop. If PDB management is disabled, the existing PDB + will remain intact, and the reconciliation loop will not be executed. By default, + PDB management is enabled. + pdbMaxUnavailable: + type: integer + description: | + Pod eviction is allowed if at most "pdbMaxUnavailable" pods are unavailable after the eviction, + i.e. even in absence of the evicted pod. For example, one can prevent all voluntary evictions + by specifying 0. This is a mutually exclusive setting with "minAvailable". + minimum: 0 + maximum: 65535 + layout: + type: object + description: | + describe current cluster layout, how much shards in cluster, how much replica in shard + allows override settings on each shard and replica separatelly + # nullable: true + properties: + replicasCount: + type: integer + description: | + how much replicas in each shards for current cluster will run in Kubernetes, + each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + every shard contains 1 replica by default" + replicas: + type: array + description: "optional, allows override top-level `chi.spec.configuration` and cluster-level `chi.spec.configuration.clusters` configuration for each replica and each shard relates to selected replica, use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and will ignore if shard-level `chi.spec.configuration.clusters.layout.shards` present + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` + shardsCount: + type: integer + description: "optional, count of shards related to current replica, you can override each shard behavior on low-level `chi.spec.configuration.clusters.layout.replicas.shards`" + minimum: 1 + shards: + type: array + description: "optional, list of shards related to current replica, will ignore if `chi.spec.configuration.clusters.layout.shards` presents" + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + zkPort: + type: integer + minimum: 1 + maximum: 65535 + raftPort: + type: integer + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and replica-level `chi.spec.configuration.clusters.layout.replicas.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates` + templates: + type: object + description: "allows define templates which will use for render Kubernetes resources like StatefulSet, ConfigMap, Service, PVC, by default, clickhouse-operator have own templates, but you can override it" + # nullable: true + properties: + hostTemplates: + type: array + description: "hostTemplate will use during apply to generate `clickhose-server` config files" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.hostTemplate`, cluster-level `chi.spec.configuration.clusters.templates.hostTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.hostTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.hostTemplate`" + type: string + portDistribution: + type: array + description: "define how will distribute numeric values of named ports in `Pod.spec.containers.ports` and clickhouse-server configs" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "type of distribution, when `Unspecified` (default value) then all listen ports on clickhouse-server configuration in all Pods will have the same value, when `ClusterScopeIndex` then ports will increment to offset from base value depends on shard and replica index inside cluster with combination of `chi.spec.templates.podTemlates.spec.HostNetwork` it allows setup ClickHouse cluster inside Kubernetes and provide access via external network bypass Kubernetes internal network" + enum: + # List PortDistributionXXX constants + - "" + - "Unspecified" + - "ClusterScopeIndex" + spec: + # Host + type: object + properties: + name: + type: string + description: "by default, hostname will generate, but this allows define custom name for each `clickhouse-server`" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + zkPort: + type: integer + minimum: 1 + maximum: 65535 + raftPort: + type: integer + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: "be careful, this part of CRD allows override template inside template, don't use it if you don't understand what you do" + podTemplates: + type: array + description: | + podTemplate will use during render `Pod` inside `StatefulSet.spec` and allows define rendered `Pod.spec`, pod scheduling distribution and pod zone + More information: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatespodtemplates + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.podTemplate`, cluster-level `chi.spec.configuration.clusters.templates.podTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.podTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.podTemplate`" + generateName: + type: string + description: "allows define format for generated `Pod` name, look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates for details about available template variables" + zone: + type: object + description: "allows define custom zone name and will separate ClickHouse `Pods` between nodes, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + #required: + # - values + properties: + key: + type: string + description: "optional, if defined, allows select kubernetes nodes by label with `name` equal `key`" + values: + type: array + description: "optional, if defined, allows select kubernetes nodes by label with `value` in `values`" + # nullable: true + items: + type: string + distribution: + type: string + description: "DEPRECATED, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + enum: + - "" + - "Unspecified" + - "OnePerHost" + podDistribution: + type: array + description: "define ClickHouse Pod distribution policy between Kubernetes Nodes inside Shard, Replica, Namespace, CHI, another ClickHouse cluster" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "you can define multiple affinity policy types" + enum: + # List PodDistributionXXX constants + - "" + - "Unspecified" + - "ClickHouseAntiAffinity" + - "ShardAntiAffinity" + - "ReplicaAntiAffinity" + - "AnotherNamespaceAntiAffinity" + - "AnotherClickHouseInstallationAntiAffinity" + - "AnotherClusterAntiAffinity" + - "MaxNumberPerNode" + - "NamespaceAffinity" + - "ClickHouseInstallationAffinity" + - "ClusterAffinity" + - "ShardAffinity" + - "ReplicaAffinity" + - "PreviousTailAffinity" + - "CircularReplication" + scope: + type: string + description: "scope for apply each podDistribution" + enum: + # list PodDistributionScopeXXX constants + - "" + - "Unspecified" + - "Shard" + - "Replica" + - "Cluster" + - "ClickHouseInstallation" + - "Namespace" + number: + type: integer + description: "define, how much ClickHouse Pods could be inside selected scope with selected distribution type" + minimum: 0 + maximum: 65535 + topologyKey: + type: string + description: | + use for inter-pod affinity look to `pod.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution.podAffinityTerm.topologyKey`, + more info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity" + metadata: + type: object + description: | + allows pass standard object's metadata from template to Pod + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify PodSpec + type: object + description: "allows define whole Pod.spec inside StaefulSet.spec, look to https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates for details" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + volumeClaimTemplates: + type: array + description: | + allows define template for rendering `PVC` kubernetes resource, which would use inside `Pod` for mount clickhouse `data`, clickhouse `logs` or something else + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + top-level `chi.spec.defaults.templates.dataVolumeClaimTemplate` or `chi.spec.defaults.templates.logVolumeClaimTemplate`, + cluster-level `chi.spec.configuration.clusters.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.templates.logVolumeClaimTemplate`, + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.shards.temlates.logVolumeClaimTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.replicas.templates.logVolumeClaimTemplate` + provisioner: + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + metadata: + type: object + description: | + allows to pass standard object's metadata from template to PVC + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + description: | + allows define all aspects of `PVC` resource + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims + # nullable: true + x-kubernetes-preserve-unknown-fields: true + serviceTemplates: + type: array + description: | + allows define template for rendering `Service` which would get endpoint from Pods which scoped chi-wide, cluster-wide, shard-wide, replica-wide level + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + chi-level `chi.spec.defaults.templates.serviceTemplate` + cluster-level `chi.spec.configuration.clusters.templates.clusterServiceTemplate` + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.shardServiceTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.replicaServiceTemplate` or `chi.spec.configuration.clusters.layout.shards.replicas.replicaServiceTemplate` + generateName: + type: string + description: | + allows define format for generated `Service` name, + look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates + for details about available template variables" + metadata: + # TODO specify ObjectMeta + type: object + description: | + allows pass standard object's metadata from template to Service + Could be use for define specificly for Cloud Provider metadata which impact to behavior of service + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify ServiceSpec + type: object + description: | + describe behavior of generated Service + More info: https://kubernetes.io/docs/concepts/services-networking/service/ + # nullable: true + x-kubernetes-preserve-unknown-fields: true diff --git a/internal/crdinstaller/crds/operator/apps.wandb.com_applications.yaml b/internal/crdinstaller/crds/operator/apps.wandb.com_applications.yaml new file mode 100644 index 00000000..67be4bc9 --- /dev/null +++ b/internal/crdinstaller/crds/operator/apps.wandb.com_applications.yaml @@ -0,0 +1,13929 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: applications.apps.wandb.com +spec: + group: apps.wandb.com + names: + kind: Application + listKind: ApplicationList + plural: applications + singular: application + scope: Namespaced + versions: + - name: v2 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + cronJobs: + items: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + concurrencyPolicy: + type: string + failedJobsHistoryLimit: + format: int32 + type: integer + jobTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + backoffLimit: + format: int32 + type: integer + backoffLimitPerIndex: + format: int32 + type: integer + completionMode: + type: string + completions: + format: int32 + type: integer + managedBy: + type: string + manualSelector: + type: boolean + maxFailedIndexes: + format: int32 + type: integer + parallelism: + format: int32 + type: integer + podFailurePolicy: + properties: + rules: + items: + properties: + action: + type: string + onExitCodes: + properties: + containerName: + type: string + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + items: + properties: + status: + type: string + type: + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + successPolicy: + properties: + rules: + items: + properties: + succeededCount: + format: int32 + type: integer + succeededIndexes: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + suspend: + type: boolean + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + x-kubernetes-list-type: atomic + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + searches: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + x-kubernetes-list-map-keys: + - ip + x-kubernetes-list-type: map + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + hostnameOverride: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + initContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + x-kubernetes-list-type: atomic + resourceClaims: + items: + properties: + name: + type: string + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + default: ext4 + type: string + kind: + type: string + readOnly: + default: false + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + default: default + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + default: /etc/ceph/keyring + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + default: xfs + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + default: ThinProvisioned + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + workloadRef: + properties: + name: + type: string + podGroup: + type: string + podGroupReplicaKey: + type: string + required: + - name + - podGroup + type: object + required: + - containers + type: object + type: object + ttlSecondsAfterFinished: + format: int32 + type: integer + required: + - template + type: object + type: object + schedule: + type: string + startingDeadlineSeconds: + format: int64 + type: integer + successfulJobsHistoryLimit: + format: int32 + type: integer + suspend: + type: boolean + timeZone: + type: string + required: + - jobTemplate + - schedule + type: object + status: + properties: + active: + items: + properties: + apiVersion: + type: string + fieldPath: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + resourceVersion: + type: string + uid: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + lastScheduleTime: + format: date-time + type: string + lastSuccessfulTime: + format: date-time + type: string + type: object + type: object + type: array + hpaTemplate: + properties: + behavior: + properties: + scaleDown: + properties: + policies: + items: + properties: + periodSeconds: + format: int32 + type: integer + type: + type: string + value: + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + type: string + stabilizationWindowSeconds: + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + properties: + policies: + items: + properties: + periodSeconds: + format: int32 + type: integer + type: + type: string + value: + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + type: string + stabilizationWindowSeconds: + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + maxReplicas: + format: int32 + type: integer + metrics: + items: + properties: + containerResource: + properties: + container: + type: string + name: + type: string + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + properties: + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + properties: + describedObject: + properties: + apiVersion: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + properties: + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + properties: + name: + type: string + target: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + type: string + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + minReplicas: + format: int32 + type: integer + scaleTargetRef: + properties: + apiVersion: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + required: + - maxReplicas + - scaleTargetRef + type: object + httpRouteTemplate: + properties: + hostnames: + items: + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + parentRefs: + items: + properties: + group: + default: gateway.networking.k8s.io + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + maxLength: 253 + minLength: 1 + type: string + namespace: + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + type: array + pathType: + type: string + paths: + items: + type: string + type: array + servicePort: + format: int32 + type: integer + required: + - parentRefs + type: object + ingressTemplate: + properties: + defaultBackend: + properties: + resource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + service: + properties: + name: + type: string + port: + properties: + name: + type: string + number: + format: int32 + type: integer + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + type: object + ingressClassName: + type: string + rules: + items: + properties: + host: + type: string + http: + properties: + paths: + items: + properties: + backend: + properties: + resource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + service: + properties: + name: + type: string + port: + properties: + name: + type: string + number: + format: int32 + type: integer + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + type: object + path: + type: string + pathType: + type: string + required: + - backend + - pathType + type: object + type: array + x-kubernetes-list-type: atomic + required: + - paths + type: object + type: object + type: array + x-kubernetes-list-type: atomic + tls: + items: + properties: + hosts: + items: + type: string + type: array + x-kubernetes-list-type: atomic + secretName: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + type: object + jobs: + items: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + backoffLimit: + format: int32 + type: integer + backoffLimitPerIndex: + format: int32 + type: integer + completionMode: + type: string + completions: + format: int32 + type: integer + managedBy: + type: string + manualSelector: + type: boolean + maxFailedIndexes: + format: int32 + type: integer + parallelism: + format: int32 + type: integer + podFailurePolicy: + properties: + rules: + items: + properties: + action: + type: string + onExitCodes: + properties: + containerName: + type: string + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + items: + properties: + status: + type: string + type: + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + successPolicy: + properties: + rules: + items: + properties: + succeededCount: + format: int32 + type: integer + succeededIndexes: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + suspend: + type: boolean + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + x-kubernetes-list-type: atomic + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + searches: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + x-kubernetes-list-map-keys: + - ip + x-kubernetes-list-type: map + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + hostnameOverride: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + initContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + x-kubernetes-list-type: atomic + resourceClaims: + items: + properties: + name: + type: string + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + default: ext4 + type: string + kind: + type: string + readOnly: + default: false + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + default: default + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + default: /etc/ceph/keyring + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + default: xfs + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + default: ThinProvisioned + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + workloadRef: + properties: + name: + type: string + podGroup: + type: string + podGroupReplicaKey: + type: string + required: + - name + - podGroup + type: object + required: + - containers + type: object + type: object + ttlSecondsAfterFinished: + format: int32 + type: integer + required: + - template + type: object + status: + properties: + active: + format: int32 + type: integer + completedIndexes: + type: string + completionTime: + format: date-time + type: string + conditions: + items: + properties: + lastProbeTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + failed: + format: int32 + type: integer + failedIndexes: + type: string + ready: + format: int32 + type: integer + startTime: + format: date-time + type: string + succeeded: + format: int32 + type: integer + terminating: + format: int32 + type: integer + uncountedTerminatedPods: + properties: + failed: + items: + type: string + type: array + x-kubernetes-list-type: set + succeeded: + items: + type: string + type: array + x-kubernetes-list-type: set + type: object + type: object + type: object + type: array + kind: + type: string + metaTemplate: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + pdbTemplate: + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + unhealthyPodEvictionPolicy: + type: string + type: object + podTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + x-kubernetes-list-type: atomic + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + searches: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + x-kubernetes-list-map-keys: + - ip + x-kubernetes-list-type: map + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + hostnameOverride: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + initContainers: + items: + properties: + args: + items: + type: string + type: array + x-kubernetes-list-type: atomic + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + envFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + stopSignal: + type: string + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + restartPolicyRules: + items: + properties: + action: + type: string + exitCodes: + properties: + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + type: object + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - devicePath + x-kubernetes-list-type: map + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + x-kubernetes-list-map-keys: + - mountPath + x-kubernetes-list-type: map + workingDir: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + x-kubernetes-list-type: atomic + resourceClaims: + items: + properties: + name: + type: string + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + default: ext4 + type: string + kind: + type: string + readOnly: + default: false + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + default: default + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + default: /etc/ceph/keyring + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + default: xfs + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + default: ThinProvisioned + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + workloadRef: + properties: + name: + type: string + podGroup: + type: string + podGroupReplicaKey: + type: string + required: + - name + - podGroup + type: object + required: + - containers + type: object + type: object + replicas: + format: int32 + type: integer + scaledObjectTemplate: + properties: + advanced: + properties: + horizontalPodAutoscalerConfig: + properties: + behavior: + properties: + scaleDown: + properties: + policies: + items: + properties: + periodSeconds: + format: int32 + type: integer + type: + type: string + value: + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + type: string + stabilizationWindowSeconds: + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + properties: + policies: + items: + properties: + periodSeconds: + format: int32 + type: integer + type: + type: string + value: + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + type: string + stabilizationWindowSeconds: + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + name: + type: string + type: object + restoreToOriginalReplicaCount: + type: boolean + scalingModifiers: + properties: + activationTarget: + type: string + formula: + type: string + metricType: + enum: + - AverageValue + - Value + type: string + target: + type: string + type: object + type: object + cooldownPeriod: + format: int32 + type: integer + fallback: + properties: + behavior: + default: static + enum: + - static + - currentReplicas + - currentReplicasIfHigher + - currentReplicasIfLower + type: string + failureThreshold: + format: int32 + type: integer + replicas: + format: int32 + type: integer + required: + - failureThreshold + - replicas + type: object + idleReplicaCount: + format: int32 + type: integer + initialCooldownPeriod: + format: int32 + type: integer + maxReplicaCount: + format: int32 + type: integer + minReplicaCount: + format: int32 + type: integer + pollingInterval: + format: int32 + type: integer + scaleTargetRef: + properties: + apiVersion: + type: string + envSourceContainerName: + type: string + kind: + type: string + name: + type: string + required: + - name + type: object + triggers: + items: + properties: + authenticationRef: + properties: + kind: + type: string + name: + type: string + required: + - name + type: object + metadata: + additionalProperties: + type: string + type: object + metricType: + type: string + name: + type: string + type: + type: string + useCachedMetrics: + type: boolean + required: + - metadata + - type + type: object + type: array + required: + - scaleTargetRef + - triggers + type: object + serviceName: + type: string + serviceTemplate: + properties: + allocateLoadBalancerNodePorts: + type: boolean + clusterIP: + type: string + clusterIPs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + externalIPs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + externalName: + type: string + externalTrafficPolicy: + type: string + healthCheckNodePort: + format: int32 + type: integer + internalTrafficPolicy: + type: string + ipFamilies: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ipFamilyPolicy: + type: string + loadBalancerClass: + type: string + loadBalancerIP: + type: string + loadBalancerSourceRanges: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ports: + items: + properties: + appProtocol: + type: string + name: + type: string + nodePort: + format: int32 + type: integer + port: + format: int32 + type: integer + protocol: + default: TCP + type: string + targetPort: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + x-kubernetes-list-map-keys: + - port + - protocol + x-kubernetes-list-type: map + publishNotReadyAddresses: + type: boolean + selector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + sessionAffinity: + type: string + sessionAffinityConfig: + properties: + clientIP: + properties: + timeoutSeconds: + format: int32 + type: integer + type: object + type: object + trafficDistribution: + type: string + type: + type: string + type: object + volumeClaimTemplates: + items: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + status: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + allocatedResourceStatuses: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: granular + allocatedResources: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + capacity: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + conditions: + items: + properties: + lastProbeTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentVolumeAttributesClassName: + type: string + modifyVolumeStatus: + properties: + status: + type: string + targetVolumeAttributesClassName: + type: string + required: + - status + type: object + phase: + type: string + type: object + type: object + type: array + type: object + status: + properties: + cronJobStatuses: + additionalProperties: + properties: + active: + items: + properties: + apiVersion: + type: string + fieldPath: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + resourceVersion: + type: string + uid: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + lastScheduleTime: + format: date-time + type: string + lastSuccessfulTime: + format: date-time + type: string + type: object + type: object + deploymentStatus: + properties: + availableReplicas: + format: int32 + type: integer + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + format: int64 + type: integer + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + terminatingReplicas: + format: int32 + type: integer + unavailableReplicas: + format: int32 + type: integer + updatedReplicas: + format: int32 + type: integer + type: object + hpaStatus: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentMetrics: + items: + properties: + containerResource: + properties: + container: + type: string + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + name: + type: string + required: + - container + - current + - name + type: object + external: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - metric + type: object + object: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + describedObject: + properties: + apiVersion: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - describedObject + - metric + type: object + pods: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - metric + type: object + resource: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + name: + type: string + required: + - current + - name + type: object + type: + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + currentReplicas: + format: int32 + type: integer + desiredReplicas: + format: int32 + type: integer + lastScaleTime: + format: date-time + type: string + observedGeneration: + format: int64 + type: integer + required: + - desiredReplicas + type: object + httpRouteStatus: + properties: + accepted: + type: boolean + type: object + ingressStatus: + properties: + loadBalancer: + properties: + ingress: + items: + properties: + hostname: + type: string + ip: + type: string + ports: + items: + properties: + error: + maxLength: 316 + 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])$ + type: string + port: + format: int32 + type: integer + protocol: + type: string + required: + - error + - port + - protocol + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + jobStatuses: + additionalProperties: + properties: + active: + format: int32 + type: integer + completedIndexes: + type: string + completionTime: + format: date-time + type: string + conditions: + items: + properties: + lastProbeTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + failed: + format: int32 + type: integer + failedIndexes: + type: string + ready: + format: int32 + type: integer + startTime: + format: date-time + type: string + succeeded: + format: int32 + type: integer + terminating: + format: int32 + type: integer + uncountedTerminatedPods: + properties: + failed: + items: + type: string + type: array + x-kubernetes-list-type: set + succeeded: + items: + type: string + type: array + x-kubernetes-list-type: set + type: object + type: object + type: object + ready: + type: boolean + rolloutStatus: + properties: + HPAReplicas: + format: int32 + type: integer + abort: + type: boolean + abortedAt: + format: date-time + type: string + alb: + properties: + canaryTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + ingress: + type: string + loadBalancer: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + stableTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + type: object + albs: + items: + properties: + canaryTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + ingress: + type: string + loadBalancer: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + stableTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + type: object + type: array + availableReplicas: + format: int32 + type: integer + blueGreen: + properties: + activeSelector: + type: string + postPromotionAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + prePromotionAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + previewSelector: + type: string + scaleUpPreviewCheckPoint: + type: boolean + type: object + canary: + properties: + currentBackgroundAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + currentExperiment: + type: string + currentStepAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + stablePingPong: + type: string + stepPluginStatuses: + items: + properties: + backoff: + type: string + disabled: + type: boolean + executions: + format: int32 + type: integer + finishedAt: + format: date-time + type: string + index: + format: int32 + type: integer + message: + type: string + name: + type: string + operation: + type: string + phase: + type: string + startedAt: + format: date-time + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + updatedAt: + format: date-time + type: string + required: + - index + - name + - operation + type: object + type: array + weights: + properties: + additional: + items: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + type: array + canary: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + stable: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + verified: + type: boolean + required: + - canary + - stable + type: object + type: object + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - lastTransitionTime + - lastUpdateTime + - message + - reason + - status + - type + type: object + type: array + controllerPause: + type: boolean + currentPodHash: + type: string + currentStepHash: + type: string + currentStepIndex: + format: int32 + type: integer + message: + type: string + observedGeneration: + type: string + pauseConditions: + items: + properties: + reason: + type: string + startTime: + format: date-time + type: string + required: + - reason + - startTime + type: object + type: array + phase: + type: string + promoteFull: + type: boolean + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + restartedAt: + format: date-time + type: string + selector: + type: string + stableRS: + type: string + updatedReplicas: + format: int32 + type: integer + workloadObservedGeneration: + type: string + type: object + serviceStatus: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + loadBalancer: + properties: + ingress: + items: + properties: + hostname: + type: string + ip: + type: string + ipMode: + type: string + ports: + items: + properties: + error: + maxLength: 316 + 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])$ + type: string + port: + format: int32 + type: integer + protocol: + type: string + required: + - error + - port + - protocol + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + statefulSetStatus: + properties: + availableReplicas: + format: int32 + type: integer + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentReplicas: + format: int32 + type: integer + currentRevision: + type: string + observedGeneration: + format: int64 + type: integer + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + updateRevision: + type: string + updatedReplicas: + format: int32 + type: integer + required: + - replicas + type: object + required: + - ready + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml b/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml new file mode 100644 index 00000000..5b5fa8d2 --- /dev/null +++ b/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml @@ -0,0 +1,6244 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: weightsandbiases.apps.wandb.com +spec: + group: apps.wandb.com + names: + kind: WeightsAndBiases + listKind: WeightsAndBiasesList + plural: weightsandbiases + shortNames: + - wandb + singular: weightsandbiases + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + chart: + type: object + x-kubernetes-preserve-unknown-fields: true + values: + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + status: + properties: + phase: + type: string + version: + type: string + type: object + type: object + served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .status.ready + name: Ready + type: boolean + - jsonPath: .status.mysqlStatus.default.state + name: MySQL + type: string + - jsonPath: .status.redisStatus.default.state + name: Redis + type: string + - jsonPath: .status.kafkaStatus.state + name: Kafka + type: string + - jsonPath: .status.objectStoreStatus.default.state + name: ObjectStore + type: string + - jsonPath: .status.clickhouseStatus.default.state + name: ClickHouse + type: string + name: v2 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + clickhouse: + additionalProperties: + properties: + externalClickhouse: + properties: + database: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + httpPort: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tcpPort: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + username: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + managedClickhouse: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + config: + properties: + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + keeper: + properties: + config: + properties: + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + replicas: + format: int32 + type: integer + storageSize: + type: string + type: object + name: + type: string + namespace: + type: string + objectStorage: + properties: + insecure: + type: boolean + prefix: + type: string + type: object + replicas: + format: int32 + type: integer + retentionPolicy: + properties: + onDelete: + default: detach + type: string + required: + - onDelete + type: object + storageSize: + type: string + telemetry: + properties: + enabled: + default: true + type: boolean + required: + - enabled + type: object + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + version: + type: string + type: object + type: object + type: object + global: + properties: + caCertsConfigMap: + type: string + customCACerts: + items: + type: string + type: array + imageRegistry: + type: string + type: object + kafka: + properties: + managedKafka: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + config: + properties: + replicationConfig: + properties: + defaultReplicationFactor: + format: int32 + type: integer + minInSyncReplicas: + format: int32 + type: integer + offsetsTopicRF: + format: int32 + type: integer + transactionStateISR: + format: int32 + type: integer + transactionStateRF: + format: int32 + type: integer + type: object + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + name: + type: string + namespace: + type: string + replicas: + format: int32 + type: integer + retentionPolicy: + properties: + onDelete: + default: detach + type: string + required: + - onDelete + type: object + skipDataRecovery: + type: boolean + storageSize: + type: string + telemetry: + properties: + enabled: + default: true + type: boolean + required: + - enabled + type: object + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + type: object + type: object + mysql: + additionalProperties: + properties: + externalMysql: + properties: + database: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslCa: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslCert: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslKey: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tls: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + username: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + managedMysql: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + config: + properties: + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + name: + type: string + namespace: + type: string + replicas: + format: int32 + type: integer + retentionPolicy: + properties: + onDelete: + default: detach + type: string + required: + - onDelete + type: object + storageSize: + type: string + telemetry: + properties: + enabled: + default: true + type: boolean + required: + - enabled + type: object + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + type: object + type: object + type: object + networking: + properties: + annotations: + additionalProperties: + type: string + type: object + gatewayAPI: + properties: + gateway: + properties: + gatewayClassName: + type: string + gatewayRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + infrastructureAnnotations: + additionalProperties: + type: string + type: object + listeners: + items: + properties: + hostname: + type: string + name: + type: string + port: + format: int32 + type: integer + protocol: + type: string + tls: + properties: + certificateRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + mode: + type: string + type: object + required: + - name + - port + - protocol + type: object + type: array + managed: + default: false + type: boolean + type: object + listenerName: + type: string + required: + - gateway + type: object + ingress: + properties: + ingressClassName: + type: string + name: + type: string + type: object + mode: + enum: + - "" + - ingress + - gateway + type: string + tls: + properties: + certManager: + properties: + clusterIssuer: + type: string + issuer: + type: string + type: object + secretName: + type: string + type: object + type: object + objectStore: + additionalProperties: + properties: + externalObjectStore: + properties: + accessKey: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + bucket: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + endpoint: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + forcePathStyle: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + path: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + provider: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + region: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secretKey: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tlsEnabled: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + managedObjectStore: + properties: + SeaweedObjectStoreSpec: + properties: + filerStorageSize: + type: string + tlsEnabled: + type: boolean + type: object + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + config: + properties: + accessKey: + type: string + minioBrowserSetting: + type: string + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + rootUser: + type: string + type: object + copies: + format: int32 + type: integer + name: + type: string + namespace: + type: string + replicas: + format: int32 + type: integer + retentionPolicy: + properties: + onDelete: + default: detach + type: string + required: + - onDelete + type: object + storageSize: + type: string + telemetry: + properties: + enabled: + default: true + type: boolean + required: + - enabled + type: object + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + type: object + type: object + type: object + redis: + additionalProperties: + properties: + externalRedis: + properties: + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslCa: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tls: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + managedRedis: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + config: + properties: + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + name: + type: string + namespace: + type: string + retentionPolicy: + properties: + onDelete: + default: detach + type: string + required: + - onDelete + type: object + sentinel: + properties: + config: + properties: + masterName: + type: string + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + enabled: + type: boolean + required: + - enabled + type: object + storageSize: + type: string + telemetry: + properties: + enabled: + default: true + type: boolean + required: + - enabled + type: object + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + type: object + type: object + type: object + requireLimits: + type: boolean + retentionPolicy: + properties: + onDelete: + default: detach + type: string + required: + - onDelete + type: object + size: + enum: + - dev + - micro + - small + - medium + - large + - xlarge + - xxlarge + type: string + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + wandb: + properties: + additionalHostnames: + items: + type: string + type: array + bucketProxy: + type: boolean + features: + additionalProperties: + type: boolean + type: object + hostname: + type: string + internalServiceAuth: + properties: + enabled: + type: boolean + oidcIssuer: + type: string + type: object + legacyOverrides: + additionalProperties: + properties: + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + type: object + type: object + license: + type: string + manifestRepository: + type: string + oidc: + properties: + authMethod: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + clientId: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + clientSecret: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + issuerUrl: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sessionLength: + type: string + type: object + probes: + properties: + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + default: "" + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + type: object + serviceAccount: + properties: + annotations: + additionalProperties: + type: string + type: object + create: + default: true + type: boolean + serviceAccountName: + default: wandb + type: string + required: + - create + type: object + version: + type: string + required: + - bucketProxy + - features + - hostname + - version + type: object + required: + - retentionPolicy + type: object + status: + properties: + clickhouseStatus: + additionalProperties: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + connection: + properties: + database: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + httpPort: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tcpPort: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + username: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + ready: + type: boolean + state: + type: string + required: + - ready + type: object + type: object + gatewayStatus: + properties: + addresses: + items: + type: string + type: array + gatewayRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + name: + type: string + ready: + type: boolean + type: object + generatedSecrets: + additionalProperties: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + ingressStatus: + properties: + loadBalancerIngress: + items: + properties: + hostname: + type: string + ip: + type: string + ipMode: + type: string + ports: + items: + properties: + error: + maxLength: 316 + 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])$ + type: string + port: + format: int32 + type: integer + protocol: + type: string + required: + - error + - port + - protocol + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + name: + type: string + type: object + kafkaStatus: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + connection: + properties: + brokerEndpoint: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + clusterID: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + ready: + type: boolean + state: + type: string + required: + - ready + type: object + mysqlStatus: + additionalProperties: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + connection: + properties: + database: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslCa: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslCert: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslKey: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tls: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + username: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + ready: + type: boolean + state: + type: string + required: + - ready + type: object + type: object + objectStoreStatus: + additionalProperties: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + connection: + properties: + accessKey: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + bucket: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + endpoint: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + forcePathStyle: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + path: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + provider: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + region: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secretKey: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tlsEnabled: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + ready: + type: boolean + state: + type: string + required: + - ready + type: object + type: object + observedGeneration: + format: int64 + type: integer + ready: + type: boolean + redisStatus: + additionalProperties: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + connection: + properties: + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + sslCa: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + tls: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + url: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + ready: + type: boolean + state: + type: string + required: + - ready + type: object + type: object + telemetryStatus: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + connection: + properties: + connectionSecret: + type: string + datadogTraceAgentHost: + type: string + datadogTraceAgentPort: + type: string + datadogTraceAgentURL: + type: string + gorillaTracer: + type: string + logsEndpoint: + type: string + logsExporter: + type: string + managedNamespace: + type: string + metricsEndpoint: + type: string + metricsExporter: + type: string + protocol: + type: string + resourceAttributes: + type: string + serviceName: + type: string + statsdAddress: + type: string + tracesEndpoint: + type: string + tracesExporter: + type: string + type: object + mode: + type: string + ready: + type: boolean + state: + type: string + required: + - ready + type: object + wandb: + properties: + applications: + additionalProperties: + properties: + cronJobStatuses: + additionalProperties: + properties: + active: + items: + properties: + apiVersion: + type: string + fieldPath: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + resourceVersion: + type: string + uid: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + lastScheduleTime: + format: date-time + type: string + lastSuccessfulTime: + format: date-time + type: string + type: object + type: object + deploymentStatus: + properties: + availableReplicas: + format: int32 + type: integer + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + format: int64 + type: integer + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + terminatingReplicas: + format: int32 + type: integer + unavailableReplicas: + format: int32 + type: integer + updatedReplicas: + format: int32 + type: integer + type: object + hpaStatus: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentMetrics: + items: + properties: + containerResource: + properties: + container: + type: string + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + name: + type: string + required: + - container + - current + - name + type: object + external: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - metric + type: object + object: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + describedObject: + properties: + apiVersion: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - describedObject + - metric + type: object + pods: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + metric: + properties: + name: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + required: + - current + - metric + type: object + resource: + properties: + current: + properties: + averageUtilization: + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + value: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + name: + type: string + required: + - current + - name + type: object + type: + type: string + required: + - type + type: object + type: array + x-kubernetes-list-type: atomic + currentReplicas: + format: int32 + type: integer + desiredReplicas: + format: int32 + type: integer + lastScaleTime: + format: date-time + type: string + observedGeneration: + format: int64 + type: integer + required: + - desiredReplicas + type: object + httpRouteStatus: + properties: + accepted: + type: boolean + type: object + ingressStatus: + properties: + loadBalancer: + properties: + ingress: + items: + properties: + hostname: + type: string + ip: + type: string + ports: + items: + properties: + error: + maxLength: 316 + 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])$ + type: string + port: + format: int32 + type: integer + protocol: + type: string + required: + - error + - port + - protocol + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + jobStatuses: + additionalProperties: + properties: + active: + format: int32 + type: integer + completedIndexes: + type: string + completionTime: + format: date-time + type: string + conditions: + items: + properties: + lastProbeTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + failed: + format: int32 + type: integer + failedIndexes: + type: string + ready: + format: int32 + type: integer + startTime: + format: date-time + type: string + succeeded: + format: int32 + type: integer + terminating: + format: int32 + type: integer + uncountedTerminatedPods: + properties: + failed: + items: + type: string + type: array + x-kubernetes-list-type: set + succeeded: + items: + type: string + type: array + x-kubernetes-list-type: set + type: object + type: object + type: object + ready: + type: boolean + rolloutStatus: + properties: + HPAReplicas: + format: int32 + type: integer + abort: + type: boolean + abortedAt: + format: date-time + type: string + alb: + properties: + canaryTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + ingress: + type: string + loadBalancer: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + stableTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + type: object + albs: + items: + properties: + canaryTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + ingress: + type: string + loadBalancer: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + stableTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + type: object + type: array + availableReplicas: + format: int32 + type: integer + blueGreen: + properties: + activeSelector: + type: string + postPromotionAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + prePromotionAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + previewSelector: + type: string + scaleUpPreviewCheckPoint: + type: boolean + type: object + canary: + properties: + currentBackgroundAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + currentExperiment: + type: string + currentStepAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + stablePingPong: + type: string + stepPluginStatuses: + items: + properties: + backoff: + type: string + disabled: + type: boolean + executions: + format: int32 + type: integer + finishedAt: + format: date-time + type: string + index: + format: int32 + type: integer + message: + type: string + name: + type: string + operation: + type: string + phase: + type: string + startedAt: + format: date-time + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + updatedAt: + format: date-time + type: string + required: + - index + - name + - operation + type: object + type: array + weights: + properties: + additional: + items: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + type: array + canary: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + stable: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + verified: + type: boolean + required: + - canary + - stable + type: object + type: object + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - lastTransitionTime + - lastUpdateTime + - message + - reason + - status + - type + type: object + type: array + controllerPause: + type: boolean + currentPodHash: + type: string + currentStepHash: + type: string + currentStepIndex: + format: int32 + type: integer + message: + type: string + observedGeneration: + type: string + pauseConditions: + items: + properties: + reason: + type: string + startTime: + format: date-time + type: string + required: + - reason + - startTime + type: object + type: array + phase: + type: string + promoteFull: + type: boolean + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + restartedAt: + format: date-time + type: string + selector: + type: string + stableRS: + type: string + updatedReplicas: + format: int32 + type: integer + workloadObservedGeneration: + type: string + type: object + serviceStatus: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + loadBalancer: + properties: + ingress: + items: + properties: + hostname: + type: string + ip: + type: string + ipMode: + type: string + ports: + items: + properties: + error: + maxLength: 316 + 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])$ + type: string + port: + format: int32 + type: integer + protocol: + type: string + required: + - error + - port + - protocol + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + statefulSetStatus: + properties: + availableReplicas: + format: int32 + type: integer + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + currentReplicas: + format: int32 + type: integer + currentRevision: + type: string + observedGeneration: + format: int64 + type: integer + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + updateRevision: + type: string + updatedReplicas: + format: int32 + type: integer + required: + - replicas + type: object + required: + - ready + type: object + default: {} + type: object + hostname: + type: string + migration: + properties: + jobs: + additionalProperties: + properties: + failed: + type: boolean + message: + type: string + name: + type: string + succeeded: + type: boolean + type: object + type: object + lastSuccessVersion: + type: string + ready: + type: boolean + reason: + type: string + version: + type: string + type: object + mysqlInit: + additionalProperties: + properties: + failed: + type: boolean + message: + type: string + name: + type: string + succeeded: + type: boolean + type: object + default: {} + type: object + required: + - hostname + type: object + required: + - observedGeneration + - ready + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redis.yaml b/internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redis.yaml new file mode 100644 index 00000000..dc080824 --- /dev/null +++ b/internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redis.yaml @@ -0,0 +1,5392 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: redis.redis.redis.opstreelabs.in +spec: + group: redis.redis.opstreelabs.in + names: + kind: Redis + listKind: RedisList + plural: redis + singular: redis + scope: Namespaced + versions: + - name: v1beta2 + schema: + openAPIV3Schema: + description: Redis is the Schema for the redis API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: RedisSpec defines the desired state of Redis + properties: + TLS: + description: TLS Configuration for redis instances + properties: + ca: + type: string + cert: + type: string + key: + type: string + secret: + description: Reference to secret which contains the certificates + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + required: + - secret + type: object + acl: + properties: + secret: + description: |- + Adapts a Secret into a volume. + + The contents of the target Secret's Data field will be presented in a volume + as files using the keys in the Data field as the file names. + Secret volumes support ownership management and SELinux relabeling. + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + type: object + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the + pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + env: + items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. Cannot + be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + hostPort: + type: integer + initContainer: + description: InitContainer for each Redis pods + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + kubernetesConfig: + description: KubernetesConfig will be the JSON struct for Basic Redis + Config + properties: + ignoreAnnotations: + items: + type: string + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + imagePullSecrets: + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + minReadySeconds: + format: int32 + type: integer + persistentVolumeClaimRetentionPolicy: + description: |- + StatefulSetPersistentVolumeClaimRetentionPolicy describes the policy used for PVCs + created from the StatefulSet VolumeClaimTemplates. + properties: + whenDeleted: + description: |- + WhenDeleted specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is deleted. The default policy + of `Retain` causes PVCs to not be affected by StatefulSet deletion. The + `Delete` policy causes those PVCs to be deleted. + type: string + whenScaled: + description: |- + WhenScaled specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is scaled down. The default + policy of `Retain` causes PVCs to not be affected by a scaledown. The + `Delete` policy causes the associated PVCs for any excess pods above + the replica count to be deleted. + type: string + type: object + redisSecret: + description: ExistingPasswordSecret is the struct to access the + existing secret + properties: + key: + type: string + name: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + service: + description: ServiceConfig define the type of service to be created + and its annotations + properties: + additional: + description: Additional config for which suffix is -additional + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + annotations: + additionalProperties: + type: string + type: object + headless: + description: Headless config for which suffix is -headless + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + serviceType: + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + updateStrategy: + description: |- + StatefulSetUpdateStrategy indicates the strategy that the StatefulSet + controller will use to perform updates. It includes any additional parameters + necessary to perform the update for the indicated strategy. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be unavailable during the update. + Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + Absolute number is calculated from percentage by rounding up. This can not be 0. + Defaults to 1. This field is alpha-level and is only honored by servers that enable the + MaxUnavailableStatefulSet feature. The field applies to all pods in the range 0 to + Replicas-1. That means if there is any unavailable pod in the range 0 to Replicas-1, it + will be counted towards MaxUnavailable. + x-kubernetes-int-or-string: true + partition: + description: |- + Partition indicates the ordinal at which the StatefulSet should be partitioned + for updates. During a rolling update, all pods from ordinal Replicas-1 to + Partition are updated. All pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: |- + Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object + required: + - image + type: object + livenessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + nodeSelector: + additionalProperties: + type: string + type: object + podSecurityContext: + description: |- + PodSecurityContext holds pod-level security attributes and common container settings. + Some fields are also present in container.securityContext. Field values of + container.securityContext take precedence over field values of PodSecurityContext. + properties: + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + priorityClassName: + type: string + readinessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + redisConfig: + description: RedisConfig defines the external configuration of Redis + properties: + additionalRedisConfig: + type: string + dynamicConfig: + items: + type: string + type: array + maxMemoryPercentOfLimit: + description: MaxMemoryPercentOfLimit is the percentage of redis + container memory limit to be used as maxmemory. + maximum: 100 + minimum: 1 + type: integer + type: object + redisExporter: + description: RedisExporter interface will have the information for + redis exporter related stuff + properties: + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + port: + default: 9121 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + serviceAccountName: + type: string + sidecars: + items: + description: Sidecar for each Redis pods + properties: + command: + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + name: + type: string + ports: + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + - name + type: object + type: array + storage: + description: Storage is the inteface to add pvc and pv support in + redis + properties: + keepAfterDelete: + type: boolean + volumeClaimTemplate: + description: PersistentVolumeClaim is a user's request for and + claim to a persistent volume + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + description: |- + Standard object's metadata. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + type: object + spec: + description: |- + spec defines the desired characteristics of a volume requested by a pod author. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over volumes to + consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference to the + PersistentVolume backing this claim. + type: string + type: object + status: + description: |- + status represents the current information/status of a persistent volume claim. + Read-only. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the actual access modes the volume backing the PVC has. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + allocatedResourceStatuses: + additionalProperties: + description: |- + When a controller receives persistentvolume claim update with ClaimResourceStatus for a resource + that it does not recognizes, then it should ignore that update and let other controllers + handle it. + type: string + description: "allocatedResourceStatuses stores status + of resource being resized for the given PVC.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nClaimResourceStatus can be in any + of following states:\n\t- ControllerResizeInProgress:\n\t\tState + set when resize controller starts resizing the volume + in control-plane.\n\t- ControllerResizeFailed:\n\t\tState + set when resize has failed in resize controller with + a terminal error.\n\t- NodeResizePending:\n\t\tState + set when resize controller has finished resizing the + volume but further resizing of\n\t\tvolume is needed + on the node.\n\t- NodeResizeInProgress:\n\t\tState set + when kubelet starts resizing the volume.\n\t- NodeResizeFailed:\n\t\tState + set when resizing has failed in kubelet with a terminal + error. Transient errors don't set\n\t\tNodeResizeFailed.\nFor + example: if expanding a PVC for more capacity - this + field can be one of the following states:\n\t- pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeFailed\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizePending\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeFailed\"\nWhen this field is not set, + it means that no resize operation is in progress for + the given PVC.\n\nA controller that receives PVC update + with previously unknown resourceName or ClaimResourceStatus\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + x-kubernetes-map-type: granular + allocatedResources: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: "allocatedResources tracks the resources + allocated to a PVC including its capacity.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nCapacity reported here may be larger + than the actual capacity when a volume expansion operation\nis + requested.\nFor storage quota, the larger value from + allocatedResources and PVC.spec.resources is used.\nIf + allocatedResources is not set, PVC.spec.resources alone + is used for quota calculation.\nIf a volume expansion + capacity request is lowered, allocatedResources is only\nlowered + if there are no expansion operations in progress and + if the actual volume capacity\nis equal or lower than + the requested capacity.\n\nA controller that receives + PVC update with previously unknown resourceName\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + capacity: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: capacity represents the actual resources + of the underlying volume. + type: object + conditions: + description: |- + conditions is the current Condition of persistent volume claim. If underlying persistent volume is being + resized then the Condition will be set to 'ResizeStarted'. + items: + description: PersistentVolumeClaimCondition contains + details about state of pvc + properties: + lastProbeTime: + description: lastProbeTime is the time we probed + the condition. + format: date-time + type: string + lastTransitionTime: + description: lastTransitionTime is the time the + condition transitioned from one status to another. + format: date-time + type: string + message: + description: message is the human-readable message + indicating details about last transition. + type: string + reason: + description: |- + reason is a unique, this should be a short, machine understandable string that gives the reason + for condition's last transition. If it reports "ResizeStarted" that means the underlying + persistent volume is being resized. + type: string + status: + type: string + type: + description: PersistentVolumeClaimConditionType + is a valid value of PersistentVolumeClaimCondition.Type + type: string + required: + - status + - type + type: object + type: array + currentVolumeAttributesClassName: + description: |- + currentVolumeAttributesClassName is the current name of the VolumeAttributesClass the PVC is using. + When unset, there is no VolumeAttributeClass applied to this PersistentVolumeClaim + This is an alpha field and requires enabling VolumeAttributesClass feature. + type: string + modifyVolumeStatus: + description: |- + ModifyVolumeStatus represents the status object of ControllerModifyVolume operation. + When this is unset, there is no ModifyVolume operation being attempted. + This is an alpha field and requires enabling VolumeAttributesClass feature. + properties: + status: + description: "status is the status of the ControllerModifyVolume + operation. It can be in any of following states:\n + - Pending\n Pending indicates that the PersistentVolumeClaim + cannot be modified due to unmet requirements, such + as\n the specified VolumeAttributesClass not existing.\n + - InProgress\n InProgress indicates that the volume + is being modified.\n - Infeasible\n Infeasible + indicates that the request has been rejected as + invalid by the CSI driver. To\n\t resolve the error, + a valid VolumeAttributesClass needs to be specified.\nNote: + New statuses can be added in the future. Consumers + should check for unknown statuses and fail appropriately." + type: string + targetVolumeAttributesClassName: + description: targetVolumeAttributesClassName is the + name of the VolumeAttributesClass the PVC currently + being reconciled + type: string + required: + - status + type: object + phase: + description: phase represents the current phase of PersistentVolumeClaim. + type: string + type: object + type: object + volumeMount: + description: Additional Volume is provided by user that is mounted + on the pods + properties: + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + volume: + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk + mount on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk + in the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in + the blob storage + type: string + fsType: + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage account Managed: azure + managed data disk (only in managed availability + set). defaults to shared' + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the + host that shares a pod's lifetime + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + items: + type: string + type: array + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default + is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: boolean + secretFile: + description: |- + secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + secretRef: + description: |- + secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: |- + defaultMode is optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers (Beta feature). + properties: + driver: + description: |- + driver is the name of the CSI driver that handles this volume. + Consult with your admin for the correct name as registered in the cluster. + type: string + fsType: + description: |- + fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated CSI driver + which will determine the default filesystem to apply. + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no secret is required. If the + secret object contains more than one secret, all secret references are passed. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. Consult your driver's documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about + the pod that should populate this volume + properties: + defaultMode: + description: |- + Optional: mode bits to use on created files by default. Must be a + Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name + and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: |- + ephemeral represents a volume that is handled by a cluster storage driver. + The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, + and deleted when the pod is removed. + + Use this if: + a) the volume is only needed while the pod runs, + b) features of normal volumes like restoring from snapshot or capacity + tracking are needed, + c) the storage driver is specified through a storage class, and + d) the storage driver supports dynamic volume provisioning through + a PersistentVolumeClaim (see EphemeralVolumeSource for more + information on the connection between this volume type + and PersistentVolumeClaim). + + Use PersistentVolumeClaim or one of the vendor-specific + APIs for volumes that persist for longer than the lifecycle + of an individual pod. + + Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to + be used that way - see the documentation of the driver for + more information. + + A pod can use both types of ephemeral volumes and + persistent volumes at the same time. + properties: + volumeClaimTemplate: + description: |- + Will be used to create a stand-alone PVC to provision the volume. + The pod in which this EphemeralVolumeSource is embedded will be the + owner of the PVC, i.e. the PVC will be deleted together with the + pod. The name of the PVC will be `<pod name>-<volume name>` where + `<volume name>` is the name from the `PodSpec.Volumes` array + entry. Pod validation will reject the pod if the concatenated name + is not valid for a PVC (for example, too long). + + An existing PVC with that name that is not owned by the pod + will *not* be used for the pod to avoid using an unrelated + volume by mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created PVC is + meant to be used by the pod, the PVC has to updated with an + owner reference to the pod once the pod exists. Normally + this should not be necessary, but it may be useful when + manually reconstructing a broken cluster. + + This field is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. + + Required, must not be nil. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. No other fields are allowed and will be rejected during + validation. + type: object + spec: + description: |- + The specification for the PersistentVolumeClaim. The entire content is + copied unchanged into the PVC that gets created from this + template. The same fields as in a PersistentVolumeClaim + are also valid here. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource + that is attached to a kubelet's host machine and then + exposed to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target + worldwide names (WWNs)' + items: + type: string + type: array + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. + items: + type: string + type: array + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to + use for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the plugin scripts. This may be + empty if no secret object is specified. If the secret object + contains more than one secret, all secrets are passed to the plugin + scripts. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. This depends on the Flocker + control service being running + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + properties: + fsType: + description: |- + fsType is filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an + EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir + into the Pod's container. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '.' is supplied, the volume directory will be the + git repository. Otherwise, if specified, the volume will contain the git repository in + the subdirectory with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the + specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: |- + glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/glusterfs/README.md + properties: + endpoints: + description: |- + endpoints is the endpoint name that details Glusterfs topology. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. This is generally + used for system agents or other privileged things that are allowed + to see the host machine. Most containers will NOT need this. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://examples.k8s.io/volumes/iscsi/README.md + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi + type: string + initiatorName: + description: |- + initiatorName is the custom iSCSI Initiator Name. + If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + <target portal>:<volume name> will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: |- + portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: |- + targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon + Controller persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources + secrets, configmaps, and downward API + properties: + defaultMode: + description: |- + defaultMode are the mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: sources is the list of volume projections + items: + description: Projection that may be projected + along with other supported volume types + properties: + clusterTrustBundle: + description: |- + ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field + of ClusterTrustBundle objects in an auto-updating file. + + Alpha, gated by the ClusterTrustBundleProjection feature gate. + + ClusterTrustBundle objects can either be selected by name, or by the + combination of signer name and a label selector. + + Kubelet performs aggressive normalization of the PEM contents written + into the pod filesystem. Esoteric PEM features such as inter-block + comments and block headers are stripped. Certificates are deduplicated. + The ordering of certificates within the file is arbitrary, and Kubelet + may change the order over time. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. Mutually-exclusive with name. If unset, + interpreted as "match nothing". If set but empty, interpreted as "match + everything". + properties: + matchExpressions: + description: matchExpressions is a + list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. If using name, then the named ClusterTrustBundle is + allowed not to exist. If using signerName, then the combination of + signerName and labelSelector is allowed to match zero + ClusterTrustBundles. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. The contents of all selected + ClusterTrustBundles will be unified and deduplicated. + type: string + required: + - path + type: object + configMap: + description: configMap information about the + configMap data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether + the ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about + the downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects + a field of the pod: only annotations, + labels, name and namespace are + supported.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file + to be created. Must not be absolute + or contain the ''..'' path. Must + be utf-8 encoded. The first item + of the relative path must not + start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + secret: + description: secret information about the + secret data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: |- + audience is the intended audience of the token. A recipient of a token + must identify itself with an identifier specified in the audience of the + token, and otherwise should reject the token. The audience defaults to the + identifier of the apiserver. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. As the token approaches expiration, the kubelet volume + plugin will proactively rotate the service account token. The kubelet will + start trying to rotate the token if the token is older than 80 percent of + its time to live or if the token is older than 24 hours.Defaults to 1 hour + and must be at least 10 minutes. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + description: quobyte represents a Quobyte mount on the + host that shares a pod's lifetime + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple entries are separated with commas) + which acts as the central registry for volumes + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set by the plugin + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references + an already created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: |- + rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/rbd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + pool: + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent + volume attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". + Default is "xfs". + type: string + gateway: + description: gateway is the host address of the + ScaleIO API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the + ScaleIO Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. If this is not provided, Login operation will fail. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL + communication with Gateway, default false + type: boolean + storageMode: + description: |- + storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage + Pool associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the + Secret or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. If not specified, default values will be attempted. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: |- + volumeNamespace specifies the scope of the volume within StorageOS. If no + namespace is specified then the Pod's namespace will be used. This allows the + Kubernetes name scoping to be mirrored within StorageOS for tighter integration. + Set VolumeName to any name to override the default behaviour. + Set to "default" if you are not using namespaces within StorageOS. + Namespaces that do not pre-exist within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy + Based Management (SPBM) profile ID associated + with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple <key,value,effect> using the matching operator <operator>. + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + required: + - kubernetesConfig + type: object + status: + description: RedisStatus defines the observed state of Redis + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redisclusters.yaml b/internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redisclusters.yaml new file mode 100644 index 00000000..aade043f --- /dev/null +++ b/internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redisclusters.yaml @@ -0,0 +1,7862 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: redisclusters.redis.redis.opstreelabs.in +spec: + group: redis.redis.opstreelabs.in + names: + kind: RedisCluster + listKind: RedisClusterList + plural: redisclusters + singular: rediscluster + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Current cluster node count + jsonPath: .spec.clusterSize + name: ClusterSize + type: integer + - description: Number of ready leader replicas + jsonPath: .status.readyLeaderReplicas + name: ReadyLeaderReplicas + type: integer + - description: Number of ready follower replicas + jsonPath: .status.readyFollowerReplicas + name: ReadyFollowerReplicas + type: integer + - description: The current state of the Redis Cluster + jsonPath: .status.state + name: State + priority: 1 + type: string + - description: Age of Cluster + jsonPath: .metadata.creationTimestamp + name: Age + priority: 1 + type: date + - description: The reason for the current state + jsonPath: .status.reason + name: Reason + priority: 1 + type: string + name: v1beta2 + schema: + openAPIV3Schema: + description: RedisCluster is the Schema for the redisclusters API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: RedisClusterSpec defines the desired state of RedisCluster + properties: + TLS: + description: TLS Configuration for redis instances + properties: + ca: + type: string + cert: + type: string + key: + type: string + secret: + description: Reference to secret which contains the certificates + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + required: + - secret + type: object + acl: + properties: + secret: + description: |- + Adapts a Secret into a volume. + + The contents of the target Secret's Data field will be presented in a volume + as files using the keys in the Data field as the file names. + Secret volumes support ownership management and SELinux relabeling. + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + type: object + clusterSize: + description: ClusterSize defines the default number of replicas for + both leader and follower when not explicitly set + format: int32 + type: integer + clusterVersion: + default: v7 + type: string + env: + items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. Cannot + be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + hostNetwork: + type: boolean + hostPort: + type: integer + initContainer: + description: InitContainer for each Redis pods + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + kubernetesConfig: + description: KubernetesConfig will be the JSON struct for Basic Redis + Config + properties: + ignoreAnnotations: + items: + type: string + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + imagePullSecrets: + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + minReadySeconds: + format: int32 + type: integer + persistentVolumeClaimRetentionPolicy: + description: |- + StatefulSetPersistentVolumeClaimRetentionPolicy describes the policy used for PVCs + created from the StatefulSet VolumeClaimTemplates. + properties: + whenDeleted: + description: |- + WhenDeleted specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is deleted. The default policy + of `Retain` causes PVCs to not be affected by StatefulSet deletion. The + `Delete` policy causes those PVCs to be deleted. + type: string + whenScaled: + description: |- + WhenScaled specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is scaled down. The default + policy of `Retain` causes PVCs to not be affected by a scaledown. The + `Delete` policy causes the associated PVCs for any excess pods above + the replica count to be deleted. + type: string + type: object + redisSecret: + description: ExistingPasswordSecret is the struct to access the + existing secret + properties: + key: + type: string + name: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + service: + description: ServiceConfig define the type of service to be created + and its annotations + properties: + additional: + description: Additional config for which suffix is -additional + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + annotations: + additionalProperties: + type: string + type: object + headless: + description: Headless config for which suffix is -headless + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + serviceType: + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + updateStrategy: + description: |- + StatefulSetUpdateStrategy indicates the strategy that the StatefulSet + controller will use to perform updates. It includes any additional parameters + necessary to perform the update for the indicated strategy. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be unavailable during the update. + Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + Absolute number is calculated from percentage by rounding up. This can not be 0. + Defaults to 1. This field is alpha-level and is only honored by servers that enable the + MaxUnavailableStatefulSet feature. The field applies to all pods in the range 0 to + Replicas-1. That means if there is any unavailable pod in the range 0 to Replicas-1, it + will be counted towards MaxUnavailable. + x-kubernetes-int-or-string: true + partition: + description: |- + Partition indicates the ordinal at which the StatefulSet should be partitioned + for updates. During a rolling update, all pods from ordinal Replicas-1 to + Partition are updated. All pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: |- + Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object + required: + - image + type: object + persistenceEnabled: + type: boolean + podSecurityContext: + description: |- + PodSecurityContext holds pod-level security attributes and common container settings. + Some fields are also present in container.securityContext. Field values of + container.securityContext take precedence over field values of PodSecurityContext. + properties: + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + port: + default: 6379 + type: integer + priorityClassName: + type: string + redisConfig: + description: RedisConfig defines the external configuration of Redis + properties: + additionalRedisConfig: + type: string + dynamicConfig: + items: + type: string + type: array + maxMemoryPercentOfLimit: + description: MaxMemoryPercentOfLimit is the percentage of redis + container memory limit to be used as maxmemory. + maximum: 100 + minimum: 1 + type: integer + type: object + redisExporter: + description: RedisExporter interface will have the information for + redis exporter related stuff + properties: + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + port: + default: 9121 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + redisFollower: + description: RedisFollower interface will have the redis follower + configuration + properties: + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + livenessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + nodeSelector: + additionalProperties: + type: string + type: object + pdb: + description: RedisPodDisruptionBudget configure a PodDisruptionBudget + on the resource (leader/follower) + properties: + enabled: + type: boolean + maxUnavailable: + format: int32 + type: integer + minAvailable: + format: int32 + type: integer + type: object + readinessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + redisConfig: + description: RedisConfig defines the external configuration of + Redis + properties: + additionalRedisConfig: + type: string + dynamicConfig: + items: + type: string + type: array + maxMemoryPercentOfLimit: + description: MaxMemoryPercentOfLimit is the percentage of + redis container memory limit to be used as maxmemory. + maximum: 100 + minimum: 1 + type: integer + type: object + replicas: + description: Replicas overrides clusterSize for follower nodes + count. If not set, uses clusterSize value + format: int32 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple <key,value,effect> using the matching operator <operator>. + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 + type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. + + This is a beta field and requires the MinDomainsInPodTopologySpread feature gate to be enabled (enabled by default). + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + If this value is nil, the behavior is equivalent to the Honor policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + If this value is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each <key, value> as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + type: object + redisLeader: + description: RedisLeader interface will have the redis leader configuration + properties: + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + livenessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + nodeSelector: + additionalProperties: + type: string + type: object + pdb: + description: RedisPodDisruptionBudget configure a PodDisruptionBudget + on the resource (leader/follower) + properties: + enabled: + type: boolean + maxUnavailable: + format: int32 + type: integer + minAvailable: + format: int32 + type: integer + type: object + readinessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + redisConfig: + description: RedisConfig defines the external configuration of + Redis + properties: + additionalRedisConfig: + type: string + dynamicConfig: + items: + type: string + type: array + maxMemoryPercentOfLimit: + description: MaxMemoryPercentOfLimit is the percentage of + redis container memory limit to be used as maxmemory. + maximum: 100 + minimum: 1 + type: integer + type: object + replicas: + description: Replicas overrides clusterSize for leader nodes count. + If not set, uses clusterSize value + format: int32 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple <key,value,effect> using the matching operator <operator>. + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 + type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. + + This is a beta field and requires the MinDomainsInPodTopologySpread feature gate to be enabled (enabled by default). + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + If this value is nil, the behavior is equivalent to the Honor policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + If this value is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each <key, value> as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + type: object + resources: + description: ResourceRequirements describes the compute resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + serviceAccountName: + type: string + sidecars: + items: + description: Sidecar for each Redis pods + properties: + command: + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + name: + type: string + ports: + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + - name + type: object + type: array + storage: + description: Node-conf needs to be added only in redis cluster + properties: + keepAfterDelete: + type: boolean + nodeConfVolume: + default: false + type: boolean + nodeConfVolumeClaimTemplate: + description: PersistentVolumeClaim is a user's request for and + claim to a persistent volume + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + description: |- + Standard object's metadata. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + type: object + spec: + description: |- + spec defines the desired characteristics of a volume requested by a pod author. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over volumes to + consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference to the + PersistentVolume backing this claim. + type: string + type: object + status: + description: |- + status represents the current information/status of a persistent volume claim. + Read-only. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the actual access modes the volume backing the PVC has. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + allocatedResourceStatuses: + additionalProperties: + description: |- + When a controller receives persistentvolume claim update with ClaimResourceStatus for a resource + that it does not recognizes, then it should ignore that update and let other controllers + handle it. + type: string + description: "allocatedResourceStatuses stores status + of resource being resized for the given PVC.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nClaimResourceStatus can be in any + of following states:\n\t- ControllerResizeInProgress:\n\t\tState + set when resize controller starts resizing the volume + in control-plane.\n\t- ControllerResizeFailed:\n\t\tState + set when resize has failed in resize controller with + a terminal error.\n\t- NodeResizePending:\n\t\tState + set when resize controller has finished resizing the + volume but further resizing of\n\t\tvolume is needed + on the node.\n\t- NodeResizeInProgress:\n\t\tState set + when kubelet starts resizing the volume.\n\t- NodeResizeFailed:\n\t\tState + set when resizing has failed in kubelet with a terminal + error. Transient errors don't set\n\t\tNodeResizeFailed.\nFor + example: if expanding a PVC for more capacity - this + field can be one of the following states:\n\t- pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeFailed\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizePending\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeFailed\"\nWhen this field is not set, + it means that no resize operation is in progress for + the given PVC.\n\nA controller that receives PVC update + with previously unknown resourceName or ClaimResourceStatus\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + x-kubernetes-map-type: granular + allocatedResources: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: "allocatedResources tracks the resources + allocated to a PVC including its capacity.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nCapacity reported here may be larger + than the actual capacity when a volume expansion operation\nis + requested.\nFor storage quota, the larger value from + allocatedResources and PVC.spec.resources is used.\nIf + allocatedResources is not set, PVC.spec.resources alone + is used for quota calculation.\nIf a volume expansion + capacity request is lowered, allocatedResources is only\nlowered + if there are no expansion operations in progress and + if the actual volume capacity\nis equal or lower than + the requested capacity.\n\nA controller that receives + PVC update with previously unknown resourceName\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + capacity: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: capacity represents the actual resources + of the underlying volume. + type: object + conditions: + description: |- + conditions is the current Condition of persistent volume claim. If underlying persistent volume is being + resized then the Condition will be set to 'ResizeStarted'. + items: + description: PersistentVolumeClaimCondition contains + details about state of pvc + properties: + lastProbeTime: + description: lastProbeTime is the time we probed + the condition. + format: date-time + type: string + lastTransitionTime: + description: lastTransitionTime is the time the + condition transitioned from one status to another. + format: date-time + type: string + message: + description: message is the human-readable message + indicating details about last transition. + type: string + reason: + description: |- + reason is a unique, this should be a short, machine understandable string that gives the reason + for condition's last transition. If it reports "ResizeStarted" that means the underlying + persistent volume is being resized. + type: string + status: + type: string + type: + description: PersistentVolumeClaimConditionType + is a valid value of PersistentVolumeClaimCondition.Type + type: string + required: + - status + - type + type: object + type: array + currentVolumeAttributesClassName: + description: |- + currentVolumeAttributesClassName is the current name of the VolumeAttributesClass the PVC is using. + When unset, there is no VolumeAttributeClass applied to this PersistentVolumeClaim + This is an alpha field and requires enabling VolumeAttributesClass feature. + type: string + modifyVolumeStatus: + description: |- + ModifyVolumeStatus represents the status object of ControllerModifyVolume operation. + When this is unset, there is no ModifyVolume operation being attempted. + This is an alpha field and requires enabling VolumeAttributesClass feature. + properties: + status: + description: "status is the status of the ControllerModifyVolume + operation. It can be in any of following states:\n + - Pending\n Pending indicates that the PersistentVolumeClaim + cannot be modified due to unmet requirements, such + as\n the specified VolumeAttributesClass not existing.\n + - InProgress\n InProgress indicates that the volume + is being modified.\n - Infeasible\n Infeasible + indicates that the request has been rejected as + invalid by the CSI driver. To\n\t resolve the error, + a valid VolumeAttributesClass needs to be specified.\nNote: + New statuses can be added in the future. Consumers + should check for unknown statuses and fail appropriately." + type: string + targetVolumeAttributesClassName: + description: targetVolumeAttributesClassName is the + name of the VolumeAttributesClass the PVC currently + being reconciled + type: string + required: + - status + type: object + phase: + description: phase represents the current phase of PersistentVolumeClaim. + type: string + type: object + type: object + volumeClaimTemplate: + description: PersistentVolumeClaim is a user's request for and + claim to a persistent volume + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + description: |- + Standard object's metadata. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + type: object + spec: + description: |- + spec defines the desired characteristics of a volume requested by a pod author. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over volumes to + consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference to the + PersistentVolume backing this claim. + type: string + type: object + status: + description: |- + status represents the current information/status of a persistent volume claim. + Read-only. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the actual access modes the volume backing the PVC has. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + allocatedResourceStatuses: + additionalProperties: + description: |- + When a controller receives persistentvolume claim update with ClaimResourceStatus for a resource + that it does not recognizes, then it should ignore that update and let other controllers + handle it. + type: string + description: "allocatedResourceStatuses stores status + of resource being resized for the given PVC.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nClaimResourceStatus can be in any + of following states:\n\t- ControllerResizeInProgress:\n\t\tState + set when resize controller starts resizing the volume + in control-plane.\n\t- ControllerResizeFailed:\n\t\tState + set when resize has failed in resize controller with + a terminal error.\n\t- NodeResizePending:\n\t\tState + set when resize controller has finished resizing the + volume but further resizing of\n\t\tvolume is needed + on the node.\n\t- NodeResizeInProgress:\n\t\tState set + when kubelet starts resizing the volume.\n\t- NodeResizeFailed:\n\t\tState + set when resizing has failed in kubelet with a terminal + error. Transient errors don't set\n\t\tNodeResizeFailed.\nFor + example: if expanding a PVC for more capacity - this + field can be one of the following states:\n\t- pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeFailed\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizePending\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeFailed\"\nWhen this field is not set, + it means that no resize operation is in progress for + the given PVC.\n\nA controller that receives PVC update + with previously unknown resourceName or ClaimResourceStatus\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + x-kubernetes-map-type: granular + allocatedResources: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: "allocatedResources tracks the resources + allocated to a PVC including its capacity.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nCapacity reported here may be larger + than the actual capacity when a volume expansion operation\nis + requested.\nFor storage quota, the larger value from + allocatedResources and PVC.spec.resources is used.\nIf + allocatedResources is not set, PVC.spec.resources alone + is used for quota calculation.\nIf a volume expansion + capacity request is lowered, allocatedResources is only\nlowered + if there are no expansion operations in progress and + if the actual volume capacity\nis equal or lower than + the requested capacity.\n\nA controller that receives + PVC update with previously unknown resourceName\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + capacity: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: capacity represents the actual resources + of the underlying volume. + type: object + conditions: + description: |- + conditions is the current Condition of persistent volume claim. If underlying persistent volume is being + resized then the Condition will be set to 'ResizeStarted'. + items: + description: PersistentVolumeClaimCondition contains + details about state of pvc + properties: + lastProbeTime: + description: lastProbeTime is the time we probed + the condition. + format: date-time + type: string + lastTransitionTime: + description: lastTransitionTime is the time the + condition transitioned from one status to another. + format: date-time + type: string + message: + description: message is the human-readable message + indicating details about last transition. + type: string + reason: + description: |- + reason is a unique, this should be a short, machine understandable string that gives the reason + for condition's last transition. If it reports "ResizeStarted" that means the underlying + persistent volume is being resized. + type: string + status: + type: string + type: + description: PersistentVolumeClaimConditionType + is a valid value of PersistentVolumeClaimCondition.Type + type: string + required: + - status + - type + type: object + type: array + currentVolumeAttributesClassName: + description: |- + currentVolumeAttributesClassName is the current name of the VolumeAttributesClass the PVC is using. + When unset, there is no VolumeAttributeClass applied to this PersistentVolumeClaim + This is an alpha field and requires enabling VolumeAttributesClass feature. + type: string + modifyVolumeStatus: + description: |- + ModifyVolumeStatus represents the status object of ControllerModifyVolume operation. + When this is unset, there is no ModifyVolume operation being attempted. + This is an alpha field and requires enabling VolumeAttributesClass feature. + properties: + status: + description: "status is the status of the ControllerModifyVolume + operation. It can be in any of following states:\n + - Pending\n Pending indicates that the PersistentVolumeClaim + cannot be modified due to unmet requirements, such + as\n the specified VolumeAttributesClass not existing.\n + - InProgress\n InProgress indicates that the volume + is being modified.\n - Infeasible\n Infeasible + indicates that the request has been rejected as + invalid by the CSI driver. To\n\t resolve the error, + a valid VolumeAttributesClass needs to be specified.\nNote: + New statuses can be added in the future. Consumers + should check for unknown statuses and fail appropriately." + type: string + targetVolumeAttributesClassName: + description: targetVolumeAttributesClassName is the + name of the VolumeAttributesClass the PVC currently + being reconciled + type: string + required: + - status + type: object + phase: + description: phase represents the current phase of PersistentVolumeClaim. + type: string + type: object + type: object + volumeMount: + description: Additional Volume is provided by user that is mounted + on the pods + properties: + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + volume: + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk + mount on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk + in the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in + the blob storage + type: string + fsType: + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage account Managed: azure + managed data disk (only in managed availability + set). defaults to shared' + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the + host that shares a pod's lifetime + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + items: + type: string + type: array + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default + is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: boolean + secretFile: + description: |- + secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + secretRef: + description: |- + secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: |- + defaultMode is optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers (Beta feature). + properties: + driver: + description: |- + driver is the name of the CSI driver that handles this volume. + Consult with your admin for the correct name as registered in the cluster. + type: string + fsType: + description: |- + fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated CSI driver + which will determine the default filesystem to apply. + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no secret is required. If the + secret object contains more than one secret, all secret references are passed. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. Consult your driver's documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about + the pod that should populate this volume + properties: + defaultMode: + description: |- + Optional: mode bits to use on created files by default. Must be a + Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name + and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: |- + ephemeral represents a volume that is handled by a cluster storage driver. + The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, + and deleted when the pod is removed. + + Use this if: + a) the volume is only needed while the pod runs, + b) features of normal volumes like restoring from snapshot or capacity + tracking are needed, + c) the storage driver is specified through a storage class, and + d) the storage driver supports dynamic volume provisioning through + a PersistentVolumeClaim (see EphemeralVolumeSource for more + information on the connection between this volume type + and PersistentVolumeClaim). + + Use PersistentVolumeClaim or one of the vendor-specific + APIs for volumes that persist for longer than the lifecycle + of an individual pod. + + Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to + be used that way - see the documentation of the driver for + more information. + + A pod can use both types of ephemeral volumes and + persistent volumes at the same time. + properties: + volumeClaimTemplate: + description: |- + Will be used to create a stand-alone PVC to provision the volume. + The pod in which this EphemeralVolumeSource is embedded will be the + owner of the PVC, i.e. the PVC will be deleted together with the + pod. The name of the PVC will be `<pod name>-<volume name>` where + `<volume name>` is the name from the `PodSpec.Volumes` array + entry. Pod validation will reject the pod if the concatenated name + is not valid for a PVC (for example, too long). + + An existing PVC with that name that is not owned by the pod + will *not* be used for the pod to avoid using an unrelated + volume by mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created PVC is + meant to be used by the pod, the PVC has to updated with an + owner reference to the pod once the pod exists. Normally + this should not be necessary, but it may be useful when + manually reconstructing a broken cluster. + + This field is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. + + Required, must not be nil. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. No other fields are allowed and will be rejected during + validation. + type: object + spec: + description: |- + The specification for the PersistentVolumeClaim. The entire content is + copied unchanged into the PVC that gets created from this + template. The same fields as in a PersistentVolumeClaim + are also valid here. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource + that is attached to a kubelet's host machine and then + exposed to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target + worldwide names (WWNs)' + items: + type: string + type: array + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. + items: + type: string + type: array + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to + use for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the plugin scripts. This may be + empty if no secret object is specified. If the secret object + contains more than one secret, all secrets are passed to the plugin + scripts. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. This depends on the Flocker + control service being running + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + properties: + fsType: + description: |- + fsType is filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an + EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir + into the Pod's container. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '.' is supplied, the volume directory will be the + git repository. Otherwise, if specified, the volume will contain the git repository in + the subdirectory with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the + specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: |- + glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/glusterfs/README.md + properties: + endpoints: + description: |- + endpoints is the endpoint name that details Glusterfs topology. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. This is generally + used for system agents or other privileged things that are allowed + to see the host machine. Most containers will NOT need this. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://examples.k8s.io/volumes/iscsi/README.md + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi + type: string + initiatorName: + description: |- + initiatorName is the custom iSCSI Initiator Name. + If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + <target portal>:<volume name> will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: |- + portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: |- + targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon + Controller persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources + secrets, configmaps, and downward API + properties: + defaultMode: + description: |- + defaultMode are the mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: sources is the list of volume projections + items: + description: Projection that may be projected + along with other supported volume types + properties: + clusterTrustBundle: + description: |- + ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field + of ClusterTrustBundle objects in an auto-updating file. + + Alpha, gated by the ClusterTrustBundleProjection feature gate. + + ClusterTrustBundle objects can either be selected by name, or by the + combination of signer name and a label selector. + + Kubelet performs aggressive normalization of the PEM contents written + into the pod filesystem. Esoteric PEM features such as inter-block + comments and block headers are stripped. Certificates are deduplicated. + The ordering of certificates within the file is arbitrary, and Kubelet + may change the order over time. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. Mutually-exclusive with name. If unset, + interpreted as "match nothing". If set but empty, interpreted as "match + everything". + properties: + matchExpressions: + description: matchExpressions is a + list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. If using name, then the named ClusterTrustBundle is + allowed not to exist. If using signerName, then the combination of + signerName and labelSelector is allowed to match zero + ClusterTrustBundles. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. The contents of all selected + ClusterTrustBundles will be unified and deduplicated. + type: string + required: + - path + type: object + configMap: + description: configMap information about the + configMap data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether + the ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about + the downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects + a field of the pod: only annotations, + labels, name and namespace are + supported.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file + to be created. Must not be absolute + or contain the ''..'' path. Must + be utf-8 encoded. The first item + of the relative path must not + start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + secret: + description: secret information about the + secret data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: |- + audience is the intended audience of the token. A recipient of a token + must identify itself with an identifier specified in the audience of the + token, and otherwise should reject the token. The audience defaults to the + identifier of the apiserver. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. As the token approaches expiration, the kubelet volume + plugin will proactively rotate the service account token. The kubelet will + start trying to rotate the token if the token is older than 80 percent of + its time to live or if the token is older than 24 hours.Defaults to 1 hour + and must be at least 10 minutes. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + description: quobyte represents a Quobyte mount on the + host that shares a pod's lifetime + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple entries are separated with commas) + which acts as the central registry for volumes + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set by the plugin + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references + an already created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: |- + rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/rbd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + pool: + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent + volume attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". + Default is "xfs". + type: string + gateway: + description: gateway is the host address of the + ScaleIO API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the + ScaleIO Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. If this is not provided, Login operation will fail. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL + communication with Gateway, default false + type: boolean + storageMode: + description: |- + storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage + Pool associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the + Secret or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. If not specified, default values will be attempted. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: |- + volumeNamespace specifies the scope of the volume within StorageOS. If no + namespace is specified then the Pod's namespace will be used. This allows the + Kubernetes name scoping to be mirrored within StorageOS for tighter integration. + Set VolumeName to any name to override the default behaviour. + Set to "default" if you are not using namespaces within StorageOS. + Namespaces that do not pre-exist within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy + Based Management (SPBM) profile ID associated + with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + type: object + required: + - clusterSize + - kubernetesConfig + type: object + status: + description: RedisClusterStatus defines the observed state of RedisCluster + properties: + readyFollowerReplicas: + default: 0 + format: int32 + type: integer + readyLeaderReplicas: + default: 0 + format: int32 + type: integer + reason: + type: string + state: + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redisreplications.yaml b/internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redisreplications.yaml new file mode 100644 index 00000000..51d2ab96 --- /dev/null +++ b/internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redisreplications.yaml @@ -0,0 +1,5593 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: redisreplications.redis.redis.opstreelabs.in +spec: + group: redis.redis.opstreelabs.in + names: + kind: RedisReplication + listKind: RedisReplicationList + plural: redisreplications + singular: redisreplication + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.masterNode + name: Master + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: Redis is the Schema for the redis API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + TLS: + description: TLS Configuration for redis instances + properties: + ca: + type: string + cert: + type: string + key: + type: string + secret: + description: Reference to secret which contains the certificates + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + required: + - secret + type: object + acl: + properties: + secret: + description: |- + Adapts a Secret into a volume. + + The contents of the target Secret's Data field will be presented in a volume + as files using the keys in the Data field as the file names. + Secret volumes support ownership management and SELinux relabeling. + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + type: object + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the + pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + clusterSize: + format: int32 + type: integer + env: + items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. Cannot + be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + hostPort: + type: integer + initContainer: + description: InitContainer for each Redis pods + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + kubernetesConfig: + description: KubernetesConfig will be the JSON struct for Basic Redis + Config + properties: + ignoreAnnotations: + items: + type: string + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + imagePullSecrets: + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + minReadySeconds: + format: int32 + type: integer + persistentVolumeClaimRetentionPolicy: + description: |- + StatefulSetPersistentVolumeClaimRetentionPolicy describes the policy used for PVCs + created from the StatefulSet VolumeClaimTemplates. + properties: + whenDeleted: + description: |- + WhenDeleted specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is deleted. The default policy + of `Retain` causes PVCs to not be affected by StatefulSet deletion. The + `Delete` policy causes those PVCs to be deleted. + type: string + whenScaled: + description: |- + WhenScaled specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is scaled down. The default + policy of `Retain` causes PVCs to not be affected by a scaledown. The + `Delete` policy causes the associated PVCs for any excess pods above + the replica count to be deleted. + type: string + type: object + redisSecret: + description: ExistingPasswordSecret is the struct to access the + existing secret + properties: + key: + type: string + name: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + service: + description: ServiceConfig define the type of service to be created + and its annotations + properties: + additional: + description: Additional config for which suffix is -additional + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + annotations: + additionalProperties: + type: string + type: object + headless: + description: Headless config for which suffix is -headless + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + serviceType: + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + updateStrategy: + description: |- + StatefulSetUpdateStrategy indicates the strategy that the StatefulSet + controller will use to perform updates. It includes any additional parameters + necessary to perform the update for the indicated strategy. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be unavailable during the update. + Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + Absolute number is calculated from percentage by rounding up. This can not be 0. + Defaults to 1. This field is alpha-level and is only honored by servers that enable the + MaxUnavailableStatefulSet feature. The field applies to all pods in the range 0 to + Replicas-1. That means if there is any unavailable pod in the range 0 to Replicas-1, it + will be counted towards MaxUnavailable. + x-kubernetes-int-or-string: true + partition: + description: |- + Partition indicates the ordinal at which the StatefulSet should be partitioned + for updates. During a rolling update, all pods from ordinal Replicas-1 to + Partition are updated. All pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: |- + Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object + required: + - image + type: object + livenessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + nodeSelector: + additionalProperties: + type: string + type: object + pdb: + description: RedisPodDisruptionBudget configure a PodDisruptionBudget + on the resource (leader/follower) + properties: + enabled: + type: boolean + maxUnavailable: + format: int32 + type: integer + minAvailable: + format: int32 + type: integer + type: object + podSecurityContext: + description: |- + PodSecurityContext holds pod-level security attributes and common container settings. + Some fields are also present in container.securityContext. Field values of + container.securityContext take precedence over field values of PodSecurityContext. + properties: + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + priorityClassName: + type: string + readinessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + redisConfig: + description: RedisConfig defines the external configuration of Redis + properties: + additionalRedisConfig: + type: string + dynamicConfig: + items: + type: string + type: array + maxMemoryPercentOfLimit: + description: MaxMemoryPercentOfLimit is the percentage of redis + container memory limit to be used as maxmemory. + maximum: 100 + minimum: 1 + type: integer + type: object + redisExporter: + description: RedisExporter interface will have the information for + redis exporter related stuff + properties: + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + port: + default: 9121 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + serviceAccountName: + type: string + sidecars: + items: + description: Sidecar for each Redis pods + properties: + command: + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + name: + type: string + ports: + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + - name + type: object + type: array + storage: + description: Storage is the inteface to add pvc and pv support in + redis + properties: + keepAfterDelete: + type: boolean + volumeClaimTemplate: + description: PersistentVolumeClaim is a user's request for and + claim to a persistent volume + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + description: |- + Standard object's metadata. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + type: object + spec: + description: |- + spec defines the desired characteristics of a volume requested by a pod author. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over volumes to + consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference to the + PersistentVolume backing this claim. + type: string + type: object + status: + description: |- + status represents the current information/status of a persistent volume claim. + Read-only. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the actual access modes the volume backing the PVC has. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + allocatedResourceStatuses: + additionalProperties: + description: |- + When a controller receives persistentvolume claim update with ClaimResourceStatus for a resource + that it does not recognizes, then it should ignore that update and let other controllers + handle it. + type: string + description: "allocatedResourceStatuses stores status + of resource being resized for the given PVC.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nClaimResourceStatus can be in any + of following states:\n\t- ControllerResizeInProgress:\n\t\tState + set when resize controller starts resizing the volume + in control-plane.\n\t- ControllerResizeFailed:\n\t\tState + set when resize has failed in resize controller with + a terminal error.\n\t- NodeResizePending:\n\t\tState + set when resize controller has finished resizing the + volume but further resizing of\n\t\tvolume is needed + on the node.\n\t- NodeResizeInProgress:\n\t\tState set + when kubelet starts resizing the volume.\n\t- NodeResizeFailed:\n\t\tState + set when resizing has failed in kubelet with a terminal + error. Transient errors don't set\n\t\tNodeResizeFailed.\nFor + example: if expanding a PVC for more capacity - this + field can be one of the following states:\n\t- pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeFailed\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizePending\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeFailed\"\nWhen this field is not set, + it means that no resize operation is in progress for + the given PVC.\n\nA controller that receives PVC update + with previously unknown resourceName or ClaimResourceStatus\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + x-kubernetes-map-type: granular + allocatedResources: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: "allocatedResources tracks the resources + allocated to a PVC including its capacity.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nCapacity reported here may be larger + than the actual capacity when a volume expansion operation\nis + requested.\nFor storage quota, the larger value from + allocatedResources and PVC.spec.resources is used.\nIf + allocatedResources is not set, PVC.spec.resources alone + is used for quota calculation.\nIf a volume expansion + capacity request is lowered, allocatedResources is only\nlowered + if there are no expansion operations in progress and + if the actual volume capacity\nis equal or lower than + the requested capacity.\n\nA controller that receives + PVC update with previously unknown resourceName\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + capacity: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: capacity represents the actual resources + of the underlying volume. + type: object + conditions: + description: |- + conditions is the current Condition of persistent volume claim. If underlying persistent volume is being + resized then the Condition will be set to 'ResizeStarted'. + items: + description: PersistentVolumeClaimCondition contains + details about state of pvc + properties: + lastProbeTime: + description: lastProbeTime is the time we probed + the condition. + format: date-time + type: string + lastTransitionTime: + description: lastTransitionTime is the time the + condition transitioned from one status to another. + format: date-time + type: string + message: + description: message is the human-readable message + indicating details about last transition. + type: string + reason: + description: |- + reason is a unique, this should be a short, machine understandable string that gives the reason + for condition's last transition. If it reports "ResizeStarted" that means the underlying + persistent volume is being resized. + type: string + status: + type: string + type: + description: PersistentVolumeClaimConditionType + is a valid value of PersistentVolumeClaimCondition.Type + type: string + required: + - status + - type + type: object + type: array + currentVolumeAttributesClassName: + description: |- + currentVolumeAttributesClassName is the current name of the VolumeAttributesClass the PVC is using. + When unset, there is no VolumeAttributeClass applied to this PersistentVolumeClaim + This is an alpha field and requires enabling VolumeAttributesClass feature. + type: string + modifyVolumeStatus: + description: |- + ModifyVolumeStatus represents the status object of ControllerModifyVolume operation. + When this is unset, there is no ModifyVolume operation being attempted. + This is an alpha field and requires enabling VolumeAttributesClass feature. + properties: + status: + description: "status is the status of the ControllerModifyVolume + operation. It can be in any of following states:\n + - Pending\n Pending indicates that the PersistentVolumeClaim + cannot be modified due to unmet requirements, such + as\n the specified VolumeAttributesClass not existing.\n + - InProgress\n InProgress indicates that the volume + is being modified.\n - Infeasible\n Infeasible + indicates that the request has been rejected as + invalid by the CSI driver. To\n\t resolve the error, + a valid VolumeAttributesClass needs to be specified.\nNote: + New statuses can be added in the future. Consumers + should check for unknown statuses and fail appropriately." + type: string + targetVolumeAttributesClassName: + description: targetVolumeAttributesClassName is the + name of the VolumeAttributesClass the PVC currently + being reconciled + type: string + required: + - status + type: object + phase: + description: phase represents the current phase of PersistentVolumeClaim. + type: string + type: object + type: object + volumeMount: + description: Additional Volume is provided by user that is mounted + on the pods + properties: + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + volume: + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk + mount on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk + in the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in + the blob storage + type: string + fsType: + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage account Managed: azure + managed data disk (only in managed availability + set). defaults to shared' + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the + host that shares a pod's lifetime + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + items: + type: string + type: array + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default + is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: boolean + secretFile: + description: |- + secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + secretRef: + description: |- + secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: |- + defaultMode is optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers (Beta feature). + properties: + driver: + description: |- + driver is the name of the CSI driver that handles this volume. + Consult with your admin for the correct name as registered in the cluster. + type: string + fsType: + description: |- + fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated CSI driver + which will determine the default filesystem to apply. + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no secret is required. If the + secret object contains more than one secret, all secret references are passed. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. Consult your driver's documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about + the pod that should populate this volume + properties: + defaultMode: + description: |- + Optional: mode bits to use on created files by default. Must be a + Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name + and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: |- + ephemeral represents a volume that is handled by a cluster storage driver. + The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, + and deleted when the pod is removed. + + Use this if: + a) the volume is only needed while the pod runs, + b) features of normal volumes like restoring from snapshot or capacity + tracking are needed, + c) the storage driver is specified through a storage class, and + d) the storage driver supports dynamic volume provisioning through + a PersistentVolumeClaim (see EphemeralVolumeSource for more + information on the connection between this volume type + and PersistentVolumeClaim). + + Use PersistentVolumeClaim or one of the vendor-specific + APIs for volumes that persist for longer than the lifecycle + of an individual pod. + + Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to + be used that way - see the documentation of the driver for + more information. + + A pod can use both types of ephemeral volumes and + persistent volumes at the same time. + properties: + volumeClaimTemplate: + description: |- + Will be used to create a stand-alone PVC to provision the volume. + The pod in which this EphemeralVolumeSource is embedded will be the + owner of the PVC, i.e. the PVC will be deleted together with the + pod. The name of the PVC will be `<pod name>-<volume name>` where + `<volume name>` is the name from the `PodSpec.Volumes` array + entry. Pod validation will reject the pod if the concatenated name + is not valid for a PVC (for example, too long). + + An existing PVC with that name that is not owned by the pod + will *not* be used for the pod to avoid using an unrelated + volume by mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created PVC is + meant to be used by the pod, the PVC has to updated with an + owner reference to the pod once the pod exists. Normally + this should not be necessary, but it may be useful when + manually reconstructing a broken cluster. + + This field is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. + + Required, must not be nil. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. No other fields are allowed and will be rejected during + validation. + type: object + spec: + description: |- + The specification for the PersistentVolumeClaim. The entire content is + copied unchanged into the PVC that gets created from this + template. The same fields as in a PersistentVolumeClaim + are also valid here. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource + that is attached to a kubelet's host machine and then + exposed to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target + worldwide names (WWNs)' + items: + type: string + type: array + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. + items: + type: string + type: array + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to + use for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the plugin scripts. This may be + empty if no secret object is specified. If the secret object + contains more than one secret, all secrets are passed to the plugin + scripts. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. This depends on the Flocker + control service being running + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + properties: + fsType: + description: |- + fsType is filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an + EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir + into the Pod's container. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '.' is supplied, the volume directory will be the + git repository. Otherwise, if specified, the volume will contain the git repository in + the subdirectory with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the + specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: |- + glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/glusterfs/README.md + properties: + endpoints: + description: |- + endpoints is the endpoint name that details Glusterfs topology. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. This is generally + used for system agents or other privileged things that are allowed + to see the host machine. Most containers will NOT need this. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://examples.k8s.io/volumes/iscsi/README.md + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi + type: string + initiatorName: + description: |- + initiatorName is the custom iSCSI Initiator Name. + If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + <target portal>:<volume name> will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: |- + portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: |- + targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon + Controller persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources + secrets, configmaps, and downward API + properties: + defaultMode: + description: |- + defaultMode are the mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: sources is the list of volume projections + items: + description: Projection that may be projected + along with other supported volume types + properties: + clusterTrustBundle: + description: |- + ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field + of ClusterTrustBundle objects in an auto-updating file. + + Alpha, gated by the ClusterTrustBundleProjection feature gate. + + ClusterTrustBundle objects can either be selected by name, or by the + combination of signer name and a label selector. + + Kubelet performs aggressive normalization of the PEM contents written + into the pod filesystem. Esoteric PEM features such as inter-block + comments and block headers are stripped. Certificates are deduplicated. + The ordering of certificates within the file is arbitrary, and Kubelet + may change the order over time. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. Mutually-exclusive with name. If unset, + interpreted as "match nothing". If set but empty, interpreted as "match + everything". + properties: + matchExpressions: + description: matchExpressions is a + list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. If using name, then the named ClusterTrustBundle is + allowed not to exist. If using signerName, then the combination of + signerName and labelSelector is allowed to match zero + ClusterTrustBundles. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. The contents of all selected + ClusterTrustBundles will be unified and deduplicated. + type: string + required: + - path + type: object + configMap: + description: configMap information about the + configMap data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether + the ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about + the downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects + a field of the pod: only annotations, + labels, name and namespace are + supported.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file + to be created. Must not be absolute + or contain the ''..'' path. Must + be utf-8 encoded. The first item + of the relative path must not + start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + secret: + description: secret information about the + secret data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: |- + audience is the intended audience of the token. A recipient of a token + must identify itself with an identifier specified in the audience of the + token, and otherwise should reject the token. The audience defaults to the + identifier of the apiserver. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. As the token approaches expiration, the kubelet volume + plugin will proactively rotate the service account token. The kubelet will + start trying to rotate the token if the token is older than 80 percent of + its time to live or if the token is older than 24 hours.Defaults to 1 hour + and must be at least 10 minutes. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + description: quobyte represents a Quobyte mount on the + host that shares a pod's lifetime + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple entries are separated with commas) + which acts as the central registry for volumes + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set by the plugin + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references + an already created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: |- + rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/rbd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + pool: + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent + volume attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". + Default is "xfs". + type: string + gateway: + description: gateway is the host address of the + ScaleIO API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the + ScaleIO Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. If this is not provided, Login operation will fail. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL + communication with Gateway, default false + type: boolean + storageMode: + description: |- + storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage + Pool associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the + Secret or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. If not specified, default values will be attempted. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: |- + volumeNamespace specifies the scope of the volume within StorageOS. If no + namespace is specified then the Pod's namespace will be used. This allows the + Kubernetes name scoping to be mirrored within StorageOS for tighter integration. + Set VolumeName to any name to override the default behaviour. + Set to "default" if you are not using namespaces within StorageOS. + Namespaces that do not pre-exist within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy + Based Management (SPBM) profile ID associated + with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple <key,value,effect> using the matching operator <operator>. + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + items: + description: TopologySpreadConstraint specifies how to spread matching + pods among the given topology. + properties: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 + type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. + + This is a beta field and requires the MinDomainsInPodTopologySpread feature gate to be enabled (enabled by default). + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + If this value is nil, the behavior is equivalent to the Honor policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + If this value is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each <key, value> as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + required: + - clusterSize + - kubernetesConfig + type: object + status: + description: RedisStatus defines the observed state of Redis + properties: + masterNode: + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redissentinels.yaml b/internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redissentinels.yaml new file mode 100644 index 00000000..b87a1753 --- /dev/null +++ b/internal/crdinstaller/crds/redis/redis.redis.opstreelabs.in_redissentinels.yaml @@ -0,0 +1,5201 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: redissentinels.redis.redis.opstreelabs.in +spec: + group: redis.redis.opstreelabs.in + names: + kind: RedisSentinel + listKind: RedisSentinelList + plural: redissentinels + singular: redissentinel + scope: Namespaced + versions: + - name: v1beta2 + schema: + openAPIV3Schema: + description: Redis is the Schema for the redis API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + TLS: + description: TLS Configuration for redis instances + properties: + ca: + type: string + cert: + type: string + key: + type: string + secret: + description: Reference to secret which contains the certificates + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + required: + - secret + type: object + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the + pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + clusterSize: + default: 3 + format: int32 + minimum: 1 + type: integer + env: + items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. Cannot + be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + hostPort: + type: integer + initContainer: + description: InitContainer for each Redis pods + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + kubernetesConfig: + description: KubernetesConfig will be the JSON struct for Basic Redis + Config + properties: + ignoreAnnotations: + items: + type: string + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + imagePullSecrets: + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + minReadySeconds: + format: int32 + type: integer + persistentVolumeClaimRetentionPolicy: + description: |- + StatefulSetPersistentVolumeClaimRetentionPolicy describes the policy used for PVCs + created from the StatefulSet VolumeClaimTemplates. + properties: + whenDeleted: + description: |- + WhenDeleted specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is deleted. The default policy + of `Retain` causes PVCs to not be affected by StatefulSet deletion. The + `Delete` policy causes those PVCs to be deleted. + type: string + whenScaled: + description: |- + WhenScaled specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is scaled down. The default + policy of `Retain` causes PVCs to not be affected by a scaledown. The + `Delete` policy causes the associated PVCs for any excess pods above + the replica count to be deleted. + type: string + type: object + redisSecret: + description: ExistingPasswordSecret is the struct to access the + existing secret + properties: + key: + type: string + name: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + service: + description: ServiceConfig define the type of service to be created + and its annotations + properties: + additional: + description: Additional config for which suffix is -additional + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + annotations: + additionalProperties: + type: string + type: object + headless: + description: Headless config for which suffix is -headless + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + serviceType: + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + updateStrategy: + description: |- + StatefulSetUpdateStrategy indicates the strategy that the StatefulSet + controller will use to perform updates. It includes any additional parameters + necessary to perform the update for the indicated strategy. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be unavailable during the update. + Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + Absolute number is calculated from percentage by rounding up. This can not be 0. + Defaults to 1. This field is alpha-level and is only honored by servers that enable the + MaxUnavailableStatefulSet feature. The field applies to all pods in the range 0 to + Replicas-1. That means if there is any unavailable pod in the range 0 to Replicas-1, it + will be counted towards MaxUnavailable. + x-kubernetes-int-or-string: true + partition: + description: |- + Partition indicates the ordinal at which the StatefulSet should be partitioned + for updates. During a rolling update, all pods from ordinal Replicas-1 to + Partition are updated. All pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: |- + Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object + required: + - image + type: object + livenessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + nodeSelector: + additionalProperties: + type: string + type: object + pdb: + description: RedisPodDisruptionBudget configure a PodDisruptionBudget + on the resource (leader/follower) + properties: + enabled: + type: boolean + maxUnavailable: + format: int32 + type: integer + minAvailable: + format: int32 + type: integer + type: object + podSecurityContext: + description: |- + PodSecurityContext holds pod-level security attributes and common container settings. + Some fields are also present in container.securityContext. Field values of + container.securityContext take precedence over field values of PodSecurityContext. + properties: + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + priorityClassName: + type: string + readinessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + redisExporter: + description: RedisExporter interface will have the information for + redis exporter related stuff + properties: + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + port: + default: 9121 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + redisSentinelConfig: + properties: + additionalSentinelConfig: + type: string + announceHostnames: + default: "no" + type: string + downAfterMilliseconds: + default: "30000" + type: string + failoverTimeout: + default: "180000" + type: string + masterGroupName: + default: myMaster + type: string + parallelSyncs: + default: "1" + type: string + quorum: + default: "2" + type: string + redisPort: + default: "6379" + type: string + redisReplicationName: + type: string + redisReplicationPassword: + description: EnvVarSource represents a source for the value of + an EnvVar. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + resolveHostnames: + default: "no" + type: string + required: + - redisReplicationName + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + serviceAccountName: + type: string + sidecars: + items: + description: Sidecar for each Redis pods + properties: + command: + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + name: + type: string + ports: + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + - name + type: object + type: array + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple <key,value,effect> using the matching operator <operator>. + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + items: + description: TopologySpreadConstraint specifies how to spread matching + pods among the given topology. + properties: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 + type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. + + This is a beta field and requires the MinDomainsInPodTopologySpread feature gate to be enabled (enabled by default). + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + If this value is nil, the behavior is equivalent to the Honor policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + If this value is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each <key, value> as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + volumeMount: + description: Additional Volume is provided by user that is mounted + on the pods + properties: + mountPath: + items: + description: VolumeMount describes a mounting of a Volume within + a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + volume: + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk mount + on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk in + the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in the + blob storage + type: string + fsType: + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage account Managed: azure managed + data disk (only in managed availability set). defaults + to shared' + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the host + that shares a pod's lifetime + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + items: + type: string + type: array + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: boolean + secretFile: + description: |- + secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + secretRef: + description: |- + secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: |- + defaultMode is optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers (Beta feature). + properties: + driver: + description: |- + driver is the name of the CSI driver that handles this volume. + Consult with your admin for the correct name as registered in the cluster. + type: string + fsType: + description: |- + fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated CSI driver + which will determine the default filesystem to apply. + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no secret is required. If the + secret object contains more than one secret, all secret references are passed. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. Consult your driver's documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about the + pod that should populate this volume + properties: + defaultMode: + description: |- + Optional: mode bits to use on created files by default. Must be a + Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field + properties: + fieldRef: + description: 'Required: Selects a field of the + pod: only annotations, labels, name and namespace + are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must not + be absolute or contain the ''..'' path. Must + be utf-8 encoded. The first item of the relative + path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: |- + ephemeral represents a volume that is handled by a cluster storage driver. + The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, + and deleted when the pod is removed. + + Use this if: + a) the volume is only needed while the pod runs, + b) features of normal volumes like restoring from snapshot or capacity + tracking are needed, + c) the storage driver is specified through a storage class, and + d) the storage driver supports dynamic volume provisioning through + a PersistentVolumeClaim (see EphemeralVolumeSource for more + information on the connection between this volume type + and PersistentVolumeClaim). + + Use PersistentVolumeClaim or one of the vendor-specific + APIs for volumes that persist for longer than the lifecycle + of an individual pod. + + Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to + be used that way - see the documentation of the driver for + more information. + + A pod can use both types of ephemeral volumes and + persistent volumes at the same time. + properties: + volumeClaimTemplate: + description: |- + Will be used to create a stand-alone PVC to provision the volume. + The pod in which this EphemeralVolumeSource is embedded will be the + owner of the PVC, i.e. the PVC will be deleted together with the + pod. The name of the PVC will be `<pod name>-<volume name>` where + `<volume name>` is the name from the `PodSpec.Volumes` array + entry. Pod validation will reject the pod if the concatenated name + is not valid for a PVC (for example, too long). + + An existing PVC with that name that is not owned by the pod + will *not* be used for the pod to avoid using an unrelated + volume by mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created PVC is + meant to be used by the pod, the PVC has to updated with an + owner reference to the pod once the pod exists. Normally + this should not be necessary, but it may be useful when + manually reconstructing a broken cluster. + + This field is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. + + Required, must not be nil. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. No other fields are allowed and will be rejected during + validation. + type: object + spec: + description: |- + The specification for the PersistentVolumeClaim. The entire content is + copied unchanged into the PVC that gets created from this + template. The same fields as in a PersistentVolumeClaim + are also valid here. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource that + is attached to a kubelet's host machine and then exposed + to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target worldwide + names (WWNs)' + items: + type: string + type: array + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. + items: + type: string + type: array + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to use + for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the plugin scripts. This may be + empty if no secret object is specified. If the secret object + contains more than one secret, all secrets are passed to the plugin + scripts. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. This depends on the Flocker + control service being running + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + properties: + fsType: + description: |- + fsType is filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an + EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir + into the Pod's container. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '.' is supplied, the volume directory will be the + git repository. Otherwise, if specified, the volume will contain the git repository in + the subdirectory with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the specified + revision. + type: string + required: + - repository + type: object + glusterfs: + description: |- + glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/glusterfs/README.md + properties: + endpoints: + description: |- + endpoints is the endpoint name that details Glusterfs topology. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. This is generally + used for system agents or other privileged things that are allowed + to see the host machine. Most containers will NOT need this. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://examples.k8s.io/volumes/iscsi/README.md + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi + type: string + initiatorName: + description: |- + initiatorName is the custom iSCSI Initiator Name. + If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + <target portal>:<volume name> will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: |- + portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: |- + targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon Controller + persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources secrets, + configmaps, and downward API + properties: + defaultMode: + description: |- + defaultMode are the mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: sources is the list of volume projections + items: + description: Projection that may be projected along + with other supported volume types + properties: + clusterTrustBundle: + description: |- + ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field + of ClusterTrustBundle objects in an auto-updating file. + + Alpha, gated by the ClusterTrustBundleProjection feature gate. + + ClusterTrustBundle objects can either be selected by name, or by the + combination of signer name and a label selector. + + Kubelet performs aggressive normalization of the PEM contents written + into the pod filesystem. Esoteric PEM features such as inter-block + comments and block headers are stripped. Certificates are deduplicated. + The ordering of certificates within the file is arbitrary, and Kubelet + may change the order over time. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. Mutually-exclusive with name. If unset, + interpreted as "match nothing". If set but empty, interpreted as "match + everything". + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. If using name, then the named ClusterTrustBundle is + allowed not to exist. If using signerName, then the combination of + signerName and labelSelector is allowed to match zero + ClusterTrustBundles. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. The contents of all selected + ClusterTrustBundles will be unified and deduplicated. + type: string + required: + - path + type: object + configMap: + description: configMap information about the configMap + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the + ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about the + downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects a field + of the pod: only annotations, labels, + name and namespace are supported.' + properties: + apiVersion: + description: Version of the schema + the FieldPath is written in terms + of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to + select in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file to + be created. Must not be absolute or + contain the ''..'' path. Must be utf-8 + encoded. The first item of the relative + path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env + vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + secret: + description: secret information about the secret + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: |- + audience is the intended audience of the token. A recipient of a token + must identify itself with an identifier specified in the audience of the + token, and otherwise should reject the token. The audience defaults to the + identifier of the apiserver. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. As the token approaches expiration, the kubelet volume + plugin will proactively rotate the service account token. The kubelet will + start trying to rotate the token if the token is older than 80 percent of + its time to live or if the token is older than 24 hours.Defaults to 1 hour + and must be at least 10 minutes. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + description: quobyte represents a Quobyte mount on the host + that shares a pod's lifetime + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple entries are separated with commas) + which acts as the central registry for volumes + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set by the plugin + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: |- + rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/rbd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + pool: + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". + Default is "xfs". + type: string + gateway: + description: gateway is the host address of the ScaleIO + API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the ScaleIO + Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. If this is not provided, Login operation will fail. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL communication + with Gateway, default false + type: boolean + storageMode: + description: |- + storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage Pool + associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret + or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. If not specified, default values will be attempted. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: |- + volumeNamespace specifies the scope of the volume within StorageOS. If no + namespace is specified then the Pod's namespace will be used. This allows the + Kubernetes name scoping to be mirrored within StorageOS for tighter integration. + Set VolumeName to any name to override the default behaviour. + Set to "default" if you are not using namespaces within StorageOS. + Namespaces that do not pre-exist within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy Based + Management (SPBM) profile ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + required: + - clusterSize + - kubernetesConfig + type: object + status: + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/crdinstaller/embed.go b/internal/crdinstaller/embed.go new file mode 100644 index 00000000..e7a373b1 --- /dev/null +++ b/internal/crdinstaller/embed.go @@ -0,0 +1,42 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package crdinstaller + +import "embed" + +// Each group's CRDs are committed under crds/<group>/ and embedded here. +// The source-of-truth lives outside this package (config/crd/bases for the +// operator's own CRDs; pkg/vendored/<vendor>/crds for upstream) — the +// `sync-crd-embed` Makefile target copies updates into these directories. + +//go:embed crds/operator/*.yaml +var operatorCRDs embed.FS + +//go:embed crds/redis/*.yaml +var redisCRDs embed.FS + +//go:embed crds/clickhouse/*.yaml +var clickhouseCRDs embed.FS + +// optionalGroups maps the value used on the --groups CLI flag to the +// matching embedded filesystem. The operator's own CRDs are NOT in here +// because they're always installed regardless of which optional groups +// are requested. +var optionalGroups = map[string]embed.FS{ + "redis": redisCRDs, + "clickhouse": clickhouseCRDs, +} diff --git a/internal/crdinstaller/render.go b/internal/crdinstaller/render.go new file mode 100644 index 00000000..d7795189 --- /dev/null +++ b/internal/crdinstaller/render.go @@ -0,0 +1,59 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package crdinstaller + +import ( + "context" + "fmt" + "io" + + "sigs.k8s.io/yaml" +) + +// Render emits every selected CRD as YAML to out, separated by `---`, in a +// deterministic order so two invocations on the same binary produce byte- +// identical output. This is the inspection path: pipe through `kubectl apply`, +// `kubectl diff`, `yq`, etc. for debugging. +func Render(_ context.Context, opts Options, out io.Writer) error { + if err := opts.Validate(); err != nil { + return err + } + crds, err := compose(opts) + if err != nil { + return err + } + for i, crd := range crds { + // Clear ManagedFields and ResourceVersion so render output stays + // reproducible and doesn't carry junk from typed defaulting. + crd.ManagedFields = nil + crd.ResourceVersion = "" + + data, err := yaml.Marshal(crd) + if err != nil { + return fmt.Errorf("marshalling %s: %w", crd.Name, err) + } + if i > 0 { + if _, err := io.WriteString(out, "---\n"); err != nil { + return err + } + } + if _, err := out.Write(data); err != nil { + return err + } + } + return nil +} diff --git a/internal/logx/attrs.go b/internal/logx/attrs.go new file mode 100644 index 00000000..a8e8eaaf --- /dev/null +++ b/internal/logx/attrs.go @@ -0,0 +1,7 @@ +package logx + +import "log/slog" + +func ErrAttr(err error) slog.Attr { + return slog.Any("error", err) +} diff --git a/internal/logx/context.go b/internal/logx/context.go new file mode 100644 index 00000000..2e29172c --- /dev/null +++ b/internal/logx/context.go @@ -0,0 +1,25 @@ +package logx + +import ( + "context" + "log/slog" +) + +type logKey struct{} + +func WithSlog(ctx context.Context, logName string) (context.Context, *slog.Logger) { + if log, ok := ctx.Value(logKey{}).(*slog.Logger); ok { + if log.Handler().(*Handler).loggerName == logName { + return ctx, log + } + } + log := NewSlogLogger(logName) + return context.WithValue(ctx, logKey{}, log), log +} + +func GetSlog(ctx context.Context) *slog.Logger { + if log, ok := ctx.Value(logKey{}).(*slog.Logger); ok { + return log + } + return NewSlogLogger("unknown") +} diff --git a/internal/logx/handler.go b/internal/logx/handler.go new file mode 100644 index 00000000..68d06a59 --- /dev/null +++ b/internal/logx/handler.go @@ -0,0 +1,76 @@ +package logx + +import ( + "context" + "log/slog" +) + +const LoggerKey = "LOGGER" + +func NewHandler(opts *Options, loggerName string) *Handler { + opts = withDefaults(opts) + + var baseHandler slog.Handler + switch opts.Format { + case JsonFormat: + baseHandler = slog.NewJSONHandler(opts.Output, opts.HandlerOptions) + case PrettyFormat: + baseHandler = BuildPrettyHandler(opts) + default: + baseHandler = slog.NewTextHandler(opts.Output, opts.HandlerOptions) + } + + defaultLevel := slog.LevelInfo + if opts.HandlerOptions.Level != nil { + defaultLevel = opts.HandlerOptions.Level.Level() + } + + return &Handler{ + handler: baseHandler, + overrides: opts.Overrides, + loggerName: loggerName, + defaultLevel: defaultLevel, + } +} + +// Handler wraps a slog.Handler and filters log records by logger name. +// It tracks the logger name through WithGroup() calls and applies +// per-logger-name level overrides. +type Handler struct { + handler slog.Handler + overrides map[string]slog.Level + loggerName string + defaultLevel slog.Level +} + +// Enabled reports whether the handler is enabled for the given level. +// It checks if the current logger name has an override level, and if so, +// compares against that. Otherwise, it uses the default level. +func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool { + if h.loggerName != "" { + if minLevel, ok := h.overrides[h.loggerName]; ok { + return level >= minLevel + } + } + return level >= h.defaultLevel +} + +// Handle passes the record through to the wrapped handler. +func (h *Handler) Handle(ctx context.Context, r slog.Record) error { + return h.handler.Handle(ctx, r) +} + +// WithAttrs returns a new handler with the given attributes added. +func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &Handler{ + handler: h.handler.WithAttrs(attrs), + overrides: h.overrides, + loggerName: h.loggerName, + defaultLevel: h.defaultLevel, + } +} + +// WithGroup returns a new handler with the given group name added to the logger name. +func (h *Handler) WithGroup(name string) slog.Handler { + return h.handler.WithGroup(name) +} diff --git a/internal/logx/options.go b/internal/logx/options.go new file mode 100644 index 00000000..bf4a8b00 --- /dev/null +++ b/internal/logx/options.go @@ -0,0 +1,68 @@ +package logx + +import ( + "io" + "log/slog" + "os" + + "github.com/go-logr/logr" +) + +type LogFormat string + +const ( + TextFormat LogFormat = "text" + JsonFormat LogFormat = "json" + PrettyFormat LogFormat = "pretty" +) + +// Options provides configuration for creating a logr or slog logger. +type Options struct { + HandlerOptions *slog.HandlerOptions + Overrides map[string]slog.Level + Output io.Writer + Format LogFormat +} + +var options *Options + +func withDefaults(opts *Options) *Options { + if opts == nil { + opts = &Options{} + } + if opts.HandlerOptions == nil { + opts.HandlerOptions = &slog.HandlerOptions{ + Level: slog.LevelInfo, + } + } + + if opts.Overrides == nil { + opts.Overrides = overrides + } + + if opts.Output == nil { + opts.Output = os.Stderr + } + + if opts.Format == "" { + opts.Format = "text" + } + + return opts +} + +// SetOptions sets the default options used by NewLogrLogger and NewSlogLogger. +func SetOptions(opts *Options) { + options = withDefaults(opts) +} + +// NewLogrLogger creates a logr.Logger backed by slog with per-name filtering. +func NewLogrLogger() logr.Logger { + return logr.FromSlogHandler(NewHandler(options, "")) +} + +// NewSlogLogger creates a slog.Logger backed by slog with per-name filtering. +func NewSlogLogger(loggerName string) *slog.Logger { + return slog.New(NewHandler(options, loggerName). + WithAttrs([]slog.Attr{slog.Any(LoggerKey, loggerName)})) +} diff --git a/internal/logx/pretty.go b/internal/logx/pretty.go new file mode 100644 index 00000000..95fd77db --- /dev/null +++ b/internal/logx/pretty.go @@ -0,0 +1,21 @@ +package logx + +import ( + "log/slog" + "strings" + "time" + + "github.com/lmittmann/tint" +) + +func BuildPrettyHandler(opts *Options) slog.Handler { + return tint.NewHandler(opts.Output, &tint.Options{ + TimeFormat: time.TimeOnly + ".000000", + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if strings.EqualFold(a.Key, LoggerKey) { + return slog.String(strings.ToUpper(a.Key), a.Value.String()) + } + return a + }, + }) +} diff --git a/internal/logx/settings.go b/internal/logx/settings.go new file mode 100644 index 00000000..48ce7bd5 --- /dev/null +++ b/internal/logx/settings.go @@ -0,0 +1,30 @@ +package logx + +import "log/slog" + +///////////////////////////////////// +// Names, Defaults + +const ( + ReconcileInfraV2 = "reconcile-infraV2" + ReconcileAppV2 = "reconcile-appV2" + Kafka = "kafka-reconcile" + Mysql = "mysql-reconcile" + Redis = "redis-reconcile" + ObjectStore = "objectstore-reconcile" + ClickHouse = "clickhouse-reconcile" + DefaultingWebhook = "defaulting-webhook" + ValidatingWebhook = "validating-webhook" +) + +var overrides = map[string]slog.Level{ + //ReconcileInfraV2: slog.LevelDebug, + //ReconcileAppV2: slog.LevelDebug, + //Kafka: slog.LevelDebug, + //Mysql: slog.LevelDebug, + //Redis: slog.LevelDebug, + ObjectStore: slog.LevelDebug, + //ClickHouse: slog.LevelDebug, + //DefaultingWebhook: slog.LevelDebug, + //ValidatingWebhook: slog.LevelDebug, +} diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go new file mode 100755 index 00000000..b7730a8b --- /dev/null +++ b/internal/metrics/metrics.go @@ -0,0 +1,48 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "sigs.k8s.io/controller-runtime/pkg/metrics" +) + +// ApplicationInfo follows the Prometheus "info" pattern: one series per +// managed W&B Application, value always 1, identity carried in the labels. +// Dashboards use this to render per-service image columns without inferring +// from cAdvisor's `image` label (which varies across runtimes and exposes +// the raw digest string). +var ApplicationInfo = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "wandb_application_info", + Help: "Currently-running image for each managed W&B Application, decomposed into repository, tag, and digest.", + }, + []string{"application_name", "namespace", "image", "tag", "digest"}, +) + +func init() { + metrics.Registry.MustRegister(ApplicationInfo) +} + +// SetApplicationInfo records the running image for a single Application. +// Existing series for the same (applicationName, namespace) pair are cleared +// first so that an image bump doesn't leave a stale row behind in dashboards. +func SetApplicationInfo(applicationName, namespace, repository, tag, digest string) { + DeleteApplicationInfo(applicationName, namespace) + ApplicationInfo.With(prometheus.Labels{ + "application_name": applicationName, + "namespace": namespace, + "image": repository, + "tag": tag, + "digest": digest, + }).Set(1) +} + +// DeleteApplicationInfo clears all series for an Application that is no +// longer in the desired set (removed from manifest or disabled by feature +// flag). Without this, dashboards would show stale apps as "currently +// running" indefinitely. +func DeleteApplicationInfo(applicationName, namespace string) { + ApplicationInfo.DeletePartialMatch(prometheus.Labels{ + "application_name": applicationName, + "namespace": namespace, + }) +} diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go new file mode 100644 index 00000000..ac12f3a5 --- /dev/null +++ b/internal/metrics/metrics_test.go @@ -0,0 +1,86 @@ +package metrics + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/assert" +) + +func gatherApplicationInfo(t *testing.T) []*dto.Metric { + t.Helper() + out := make(chan prometheus.Metric, 64) + go func() { + ApplicationInfo.Collect(out) + close(out) + }() + var metrics []*dto.Metric + for m := range out { + dtoMetric := &dto.Metric{} + assert.NoError(t, m.Write(dtoMetric)) + metrics = append(metrics, dtoMetric) + } + return metrics +} + +func labelMap(m *dto.Metric) map[string]string { + out := make(map[string]string, len(m.Label)) + for _, lp := range m.Label { + out[lp.GetName()] = lp.GetValue() + } + return out +} + +func TestSetApplicationInfo_EmitsExpectedLabels(t *testing.T) { + t.Cleanup(ApplicationInfo.Reset) + ApplicationInfo.Reset() + + SetApplicationInfo("api", "wandb", "us-docker.pkg.dev/wandb/api", "v1.0.0", "sha256:abc") + + got := gatherApplicationInfo(t) + assert.Len(t, got, 1) + assert.Equal(t, 1.0, got[0].Gauge.GetValue()) + assert.Equal(t, map[string]string{ + "application_name": "api", + "namespace": "wandb", + "image": "us-docker.pkg.dev/wandb/api", + "tag": "v1.0.0", + "digest": "sha256:abc", + }, labelMap(got[0])) +} + +func TestSetApplicationInfo_TagBumpClearsStaleSeries(t *testing.T) { + t.Cleanup(ApplicationInfo.Reset) + ApplicationInfo.Reset() + + SetApplicationInfo("api", "wandb", "repo", "v1.0", "") + SetApplicationInfo("api", "wandb", "repo", "v1.1", "") + + got := gatherApplicationInfo(t) + assert.Len(t, got, 1, "stale series from previous tag must be cleared on re-set") + assert.Equal(t, "v1.1", labelMap(got[0])["tag"]) +} + +func TestDeleteApplicationInfo_OnlyMatchesNameAndNamespace(t *testing.T) { + t.Cleanup(ApplicationInfo.Reset) + ApplicationInfo.Reset() + + SetApplicationInfo("api", "wandb-a", "repo", "v1", "") + SetApplicationInfo("api", "wandb-b", "repo", "v1", "") + SetApplicationInfo("executor", "wandb-a", "repo", "v1", "") + + DeleteApplicationInfo("api", "wandb-a") + + got := gatherApplicationInfo(t) + assert.Len(t, got, 2, "delete must scope to (application_name, namespace) — leaving other apps and other namespaces intact") + + remaining := make(map[string]string) + for _, m := range got { + lm := labelMap(m) + remaining[lm["application_name"]+"/"+lm["namespace"]] = lm["tag"] + } + assert.Contains(t, remaining, "api/wandb-b") + assert.Contains(t, remaining, "executor/wandb-a") + assert.NotContains(t, remaining, "api/wandb-a") +} diff --git a/internal/probes/probes.go b/internal/probes/probes.go new file mode 100644 index 00000000..eeec28e0 --- /dev/null +++ b/internal/probes/probes.go @@ -0,0 +1,131 @@ +package probes + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func ApplyTemplate(target, template *corev1.Probe) *corev1.Probe { + if template == nil { + return Clone(target) + } + if target == nil { + return template.DeepCopy() + } + + result := target.DeepCopy() + MergeMissing(result, template) + return result +} + +func MergeMissing(base, overlay *corev1.Probe) { + if base == nil || overlay == nil { + return + } + + MergeHandlerMissing(&base.ProbeHandler, overlay.ProbeHandler) + + if base.InitialDelaySeconds == 0 { + base.InitialDelaySeconds = overlay.InitialDelaySeconds + } + if base.TimeoutSeconds == 0 { + base.TimeoutSeconds = overlay.TimeoutSeconds + } + if base.PeriodSeconds == 0 { + base.PeriodSeconds = overlay.PeriodSeconds + } + if base.SuccessThreshold == 0 { + base.SuccessThreshold = overlay.SuccessThreshold + } + if base.FailureThreshold == 0 { + base.FailureThreshold = overlay.FailureThreshold + } + if base.TerminationGracePeriodSeconds == nil && overlay.TerminationGracePeriodSeconds != nil { + value := *overlay.TerminationGracePeriodSeconds + base.TerminationGracePeriodSeconds = &value + } +} + +func MergeHandlerMissing(base *corev1.ProbeHandler, overlay corev1.ProbeHandler) { + if base == nil { + return + } + + if !HasHandler(&corev1.Probe{ProbeHandler: *base}) { + overlayProbe := (&corev1.Probe{ProbeHandler: overlay}).DeepCopy() + base.Exec = overlayProbe.Exec + base.HTTPGet = overlayProbe.HTTPGet + base.TCPSocket = overlayProbe.TCPSocket + base.GRPC = overlayProbe.GRPC + return + } + + if base.Exec != nil && overlay.Exec != nil && len(base.Exec.Command) == 0 { + base.Exec.Command = append([]string(nil), overlay.Exec.Command...) + } + if base.HTTPGet != nil && overlay.HTTPGet != nil { + mergeHTTPGetActionMissing(base.HTTPGet, overlay.HTTPGet) + } + if base.TCPSocket != nil && overlay.TCPSocket != nil { + mergeTCPSocketActionMissing(base.TCPSocket, overlay.TCPSocket) + } + if base.GRPC != nil && overlay.GRPC != nil { + mergeGRPCActionMissing(base.GRPC, overlay.GRPC) + } +} + +func mergeHTTPGetActionMissing(base, overlay *corev1.HTTPGetAction) { + if base.Path == "" { + base.Path = overlay.Path + } + if IntOrStringEmpty(base.Port) { + base.Port = overlay.Port + } + if base.Host == "" { + base.Host = overlay.Host + } + if base.Scheme == "" { + base.Scheme = overlay.Scheme + } + if len(base.HTTPHeaders) == 0 && len(overlay.HTTPHeaders) > 0 { + base.HTTPHeaders = append([]corev1.HTTPHeader(nil), overlay.HTTPHeaders...) + } +} + +func mergeTCPSocketActionMissing(base, overlay *corev1.TCPSocketAction) { + if IntOrStringEmpty(base.Port) { + base.Port = overlay.Port + } + if base.Host == "" { + base.Host = overlay.Host + } +} + +func mergeGRPCActionMissing(base, overlay *corev1.GRPCAction) { + if base.Port == 0 { + base.Port = overlay.Port + } + if base.Service == nil && overlay.Service != nil { + service := *overlay.Service + base.Service = &service + } +} + +func Clone(probe *corev1.Probe) *corev1.Probe { + if probe == nil { + return nil + } + return probe.DeepCopy() +} + +func HasHandler(probe *corev1.Probe) bool { + return probe != nil && + (probe.Exec != nil || + probe.HTTPGet != nil || + probe.TCPSocket != nil || + probe.GRPC != nil) +} + +func IntOrStringEmpty(value intstr.IntOrString) bool { + return value.StrVal == "" && value.IntVal == 0 +} diff --git a/internal/webhook/v2/application_webhook.go b/internal/webhook/v2/application_webhook.go new file mode 100644 index 00000000..4fc4d39a --- /dev/null +++ b/internal/webhook/v2/application_webhook.go @@ -0,0 +1,145 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + appsv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/logx" +) + +// SetupApplicationWebhookWithManager registers the webhook for Application in the manager. +func SetupApplicationWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&appsv2.Application{}). + WithValidator(&ApplicationCustomValidator{}). + WithDefaulter(&ApplicationCustomDefaulter{}). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-apps-wandb-com-v2-application,mutating=true,failurePolicy=fail,sideEffects=None,groups=apps.wandb.com,resources=applications,verbs=create;update,versions=v2,name=mapplication-v2.kb.io,admissionReviewVersions=v1 + +// ApplicationCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind Application when those are created or updated. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as it is used only for temporary operations and does not need to be deeply copied. +type ApplicationCustomDefaulter struct { + // TODO(user): Add more fields as needed for defaulting +} + +var _ webhook.CustomDefaulter = &ApplicationCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Application. +func (d *ApplicationCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + logger := logx.GetSlog(ctx) + application, ok := obj.(*appsv2.Application) + + if !ok { + return fmt.Errorf("expected an Application object but got %T", obj) + } + logger.Debug("Defaulting for Application", "name", application.GetName()) + + // TODO(user): fill in your defaulting logic. + + return nil +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'. +// +kubebuilder:webhook:path=/validate-apps-wandb-com-v2-application,mutating=false,failurePolicy=fail,sideEffects=None,groups=apps.wandb.com,resources=applications,verbs=create;update,versions=v2,name=vapplication-v2.kb.io,admissionReviewVersions=v1 + +// ApplicationCustomValidator struct is responsible for validating the Application resource +// when it is created, updated, or deleted. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as this struct is used only for temporary operations and does not need to be deeply copied. +type ApplicationCustomValidator struct { + // TODO(user): Add more fields as needed for validation +} + +var _ webhook.CustomValidator = &ApplicationCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Application. +func (v *ApplicationCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + logger := logx.GetSlog(ctx) + application, ok := obj.(*appsv2.Application) + if !ok { + return nil, fmt.Errorf("expected a Application object but got %T", obj) + } + logger.Debug("Validation for Application upon creation", "name", application.GetName()) + + if err := v.validateHPAAndReplicas(application); err != nil { + return nil, err + } + + return nil, nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Application. +func (v *ApplicationCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + logger := logx.GetSlog(ctx) + application, ok := newObj.(*appsv2.Application) + if !ok { + return nil, fmt.Errorf("expected a Application object for the newObj but got %T", newObj) + } + logger.Debug("Validation for Application upon update", "name", application.GetName()) + + if err := v.validateHPAAndReplicas(application); err != nil { + return nil, err + } + + applicationOld, ok := oldObj.(*appsv2.Application) + if !ok { + return nil, fmt.Errorf("expected a Application object for the oldObj but got %T", oldObj) + } + + if applicationOld.Spec.Kind != application.Spec.Kind { + return nil, fmt.Errorf("cannot change kind of an Application") + } + + return nil, nil +} + +func (v *ApplicationCustomValidator) validateHPAAndReplicas(app *appsv2.Application) error { + if app.Spec.Replicas != nil && app.Spec.HpaTemplate != nil { + return fmt.Errorf("cannot specify both replicas and hpaTemplate") + } + return nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Application. +func (v *ApplicationCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + logger := logx.GetSlog(ctx) + application, ok := obj.(*appsv2.Application) + if !ok { + return nil, fmt.Errorf("expected a Application object but got %T", obj) + } + logger.Debug("Validation for Application upon deletion", "name", application.GetName()) + + // TODO(user): fill in your validation logic upon object deletion. + + return nil, nil +} diff --git a/internal/webhook/v2/application_webhook_test.go b/internal/webhook/v2/application_webhook_test.go new file mode 100644 index 00000000..a250c452 --- /dev/null +++ b/internal/webhook/v2/application_webhook_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv2 "github.com/wandb/operator/api/v2" + autoscalingv2 "k8s.io/api/autoscaling/v2" +) + +var _ = Describe("Application Webhook", func() { + var ( + obj *appsv2.Application + oldObj *appsv2.Application + validator ApplicationCustomValidator + defaulter ApplicationCustomDefaulter + ) + + BeforeEach(func() { + obj = &appsv2.Application{} + oldObj = &appsv2.Application{} + validator = ApplicationCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + defaulter = ApplicationCustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + // TODO (user): Add any setup logic common to all tests + }) + + AfterEach(func() { + // TODO (user): Add any teardown logic common to all tests + }) + + Context("When creating Application under Defaulting Webhook", func() { + // TODO (user): Add logic for defaulting webhooks + // Example: + // It("Should apply defaults when a required field is empty", func() { + // By("simulating a scenario where defaults should be applied") + // obj.SomeFieldWithDefault = "" + // By("calling the Default method to apply defaults") + // defaulter.Default(ctx, obj) + // By("checking that the default values are set") + // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) + // }) + }) + + Context("When creating or updating Application under Validating Webhook", func() { + ctx := context.Background() + + It("Should deny if both replicas and hpaTemplate are provided", func() { + var replicas int32 = 3 + obj.Spec.Replicas = &replicas + obj.Spec.HpaTemplate = &autoscalingv2.HorizontalPodAutoscalerSpec{ + MaxReplicas: 10, + } + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("cannot specify both replicas and hpaTemplate")) + + _, err = validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("cannot specify both replicas and hpaTemplate")) + }) + + It("Should admit if only replicas is provided", func() { + var replicas int32 = 3 + obj.Spec.Replicas = &replicas + obj.Spec.HpaTemplate = nil + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + + _, err = validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).NotTo(HaveOccurred()) + }) + + It("Should admit if only hpaTemplate is provided", func() { + obj.Spec.Replicas = nil + obj.Spec.HpaTemplate = &autoscalingv2.HorizontalPodAutoscalerSpec{ + MaxReplicas: 10, + } + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + + _, err = validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).NotTo(HaveOccurred()) + }) + }) + +}) diff --git a/internal/webhook/v2/webhook_suite_test.go b/internal/webhook/v2/webhook_suite_test.go new file mode 100644 index 00000000..1d49d69c --- /dev/null +++ b/internal/webhook/v2/webhook_suite_test.go @@ -0,0 +1,167 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "os" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + appsv2 "github.com/wandb/operator/api/v2" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + ctx context.Context + cancel context.CancelFunc + k8sClient client.Client + cfg *rest.Config + testEnv *envtest.Environment +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + var err error + err = appsv2.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, + }, + } + + // Retrieve the first found binary directory to allow running tests from IDEs + if getFirstFoundEnvTestBinaryDir() != "" { + testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() + } + + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager. + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + WebhookServer: webhook.NewServer(webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), + LeaderElection: false, + Metrics: metricsserver.Options{BindAddress: "0"}, + }) + Expect(err).NotTo(HaveOccurred()) + + err = SetupApplicationWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + err = SetupWeightsAndBiasesWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready. + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + + return conn.Close() + }).Should(Succeed()) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) + +// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. +// ENVTEST-based tests depend on specific binaries, usually located in paths set by +// controller-runtime. When running tests directly (e.g., via an IDE) without using +// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. +// +// This function streamlines the process by finding the required binaries, similar to +// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are +// properly set up, run 'make setup-envtest' beforehand. +func getFirstFoundEnvTestBinaryDir() string { + basePath := filepath.Join("..", "..", "..", "bin", "k8s") + entries, err := os.ReadDir(basePath) + if err != nil { + logf.Log.Error(err, "Failed to read directory", "path", basePath) + return "" + } + for _, entry := range entries { + if entry.IsDir() { + return filepath.Join(basePath, entry.Name()) + } + } + return "" +} diff --git a/internal/webhook/v2/weightsandbiases_defaulter_clickhouse_test.go b/internal/webhook/v2/weightsandbiases_defaulter_clickhouse_test.go new file mode 100644 index 00000000..65b27ce8 --- /dev/null +++ b/internal/webhook/v2/weightsandbiases_defaulter_clickhouse_test.go @@ -0,0 +1,149 @@ +package v2 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + g "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/infra/managed/clickhouse/altinity" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("WeightsAndBiasesCustomDefaulter - ClickHouse", func() { + var ( + ctx context.Context + defaulter WeightsAndBiasesCustomDefaulter + ) + + BeforeEach(func() { + ctx = context.Background() + defaulter = WeightsAndBiasesCustomDefaulter{} + }) + + It("defaults ClickHouse namespace to the parent namespace", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ClickHouse: map[string]apiv2.ClickHouseSpec{apiv2.DefaultInstanceName: {ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}}}}, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.Namespace).To(g.Equal("test-namespace")) + }) + + It("preserves a custom ClickHouse namespace", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + ClickHouse: map[string]apiv2.ClickHouseSpec{apiv2.DefaultInstanceName: {ManagedClickHouse: &apiv2.ManagedClickHouseSpec{Namespace: "custom-clickhouse-namespace"}}}, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.Namespace).To(g.Equal("custom-clickhouse-namespace")) + }) + + It("does not mutate unrelated ClickHouse fields", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + ClickHouse: map[string]apiv2.ClickHouseSpec{ + apiv2.DefaultInstanceName: { + ManagedClickHouse: &apiv2.ManagedClickHouseSpec{ + StorageSize: "100Gi", + Replicas: 2, + Version: "24.1", + }, + }, + }, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.StorageSize).To(g.Equal("100Gi")) + g.Expect(wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.Replicas).To(g.Equal(int32(2))) + g.Expect(wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.Version).To(g.Equal("24.1")) + }) + + It("defaults the plain '<cr>-chi' name for CR names that fit", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ClickHouse: map[string]apiv2.ClickHouseSpec{apiv2.DefaultInstanceName: {ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}}}}, + } + + g.Expect(defaulter.Default(ctx, wandb)).To(g.Succeed()) + g.Expect(wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.Name).To(g.Equal("test-wandb-chi")) + }) + + It("keys non-default instance names before the suffix", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ClickHouse: map[string]apiv2.ClickHouseSpec{ + apiv2.DefaultInstanceName: {ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}}, + "analytics": {ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}}, + }}, + } + + g.Expect(defaulter.Default(ctx, wandb)).To(g.Succeed()) + g.Expect(wandb.Spec.ClickHouse["analytics"].ManagedClickHouse.Name).To(g.Equal("test-wandb-analytics-chi")) + }) + + It("defaults a deployable name for CR names the plain default would wedge", func() { + // 32 chars: "<cr>-chi" would overflow the derived per-host volume names + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb-integration-environments-2", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ClickHouse: map[string]apiv2.ClickHouseSpec{apiv2.DefaultInstanceName: {ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}}}}, + } + + g.Expect(defaulter.Default(ctx, wandb)).To(g.Succeed()) + + managed := wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse + g.Expect(managed.Name).To(g.HaveSuffix("-chi")) + g.Expect(len(managed.Name)).To(g.BeNumerically("<=", altinity.MaxSpecNameLength())) + g.Expect(altinity.ValidateDerivedNames(managed)).To(g.Succeed()) + + // persisted in the spec, so it must be deterministic + again := &apiv2.WeightsAndBiases{ + ObjectMeta: wandb.ObjectMeta, + Spec: apiv2.WeightsAndBiasesSpec{ClickHouse: map[string]apiv2.ClickHouseSpec{apiv2.DefaultInstanceName: {ManagedClickHouse: &apiv2.ManagedClickHouseSpec{}}}}, + } + g.Expect(defaulter.Default(ctx, again)).To(g.Succeed()) + g.Expect(again.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.Name).To(g.Equal(managed.Name)) + }) + + It("keeps plain default names for the other infra at CR lengths only ClickHouse would break", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb-legacy-overrides-v1", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + MySQL: map[string]apiv2.MySQLSpec{apiv2.DefaultInstanceName: {ManagedMysql: &apiv2.ManagedMysqlSpec{}}}, + Redis: map[string]apiv2.RedisSpec{apiv2.DefaultInstanceName: {ManagedRedis: &apiv2.ManagedRedisSpec{}}}, + Kafka: apiv2.KafkaSpec{ManagedKafka: &apiv2.ManagedKafkaSpec{}}, + ObjectStore: map[string]apiv2.ObjectStoreSpec{apiv2.DefaultInstanceName: {ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{}}}, + }, + } + + g.Expect(defaulter.Default(ctx, wandb)).To(g.Succeed()) + g.Expect(wandb.Spec.MySQL[apiv2.DefaultInstanceName].ManagedMysql.Name).To(g.Equal("wandb-legacy-overrides-v1-mysql")) + g.Expect(wandb.Spec.Redis[apiv2.DefaultInstanceName].ManagedRedis.Name).To(g.Equal("wandb-legacy-overrides-v1-redis")) + g.Expect(wandb.Spec.Kafka.ManagedKafka.Name).To(g.Equal("wandb-legacy-overrides-v1-kafka")) + g.Expect(wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.Name).To(g.Equal("wandb-legacy-overrides-v1-seaweedfs")) + }) + + It("does not apply defaults when ExternalClickhouse is present", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + ClickHouse: map[string]apiv2.ClickHouseSpec{ + apiv2.DefaultInstanceName: {ExternalClickHouse: &apiv2.ClickHouseConnection{}}, + }, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse).To(g.BeNil()) + }) +}) diff --git a/internal/webhook/v2/weightsandbiases_defaulter_kafka_test.go b/internal/webhook/v2/weightsandbiases_defaulter_kafka_test.go new file mode 100644 index 00000000..74c1d862 --- /dev/null +++ b/internal/webhook/v2/weightsandbiases_defaulter_kafka_test.go @@ -0,0 +1,73 @@ +package v2 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + g "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("WeightsAndBiasesCustomDefaulter - Kafka", func() { + var ( + ctx context.Context + defaulter WeightsAndBiasesCustomDefaulter + ) + + BeforeEach(func() { + ctx = context.Background() + defaulter = WeightsAndBiasesCustomDefaulter{} + }) + + It("defaults Kafka namespace to the parent namespace", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{Kafka: apiv2.KafkaSpec{ManagedKafka: &apiv2.ManagedKafkaSpec{}}}, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.Kafka.ManagedKafka.Namespace).To(g.Equal("test-namespace")) + }) + + It("preserves a custom Kafka namespace", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + Kafka: apiv2.KafkaSpec{ManagedKafka: &apiv2.ManagedKafkaSpec{Namespace: "custom-kafka-namespace"}}, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.Kafka.ManagedKafka.Namespace).To(g.Equal("custom-kafka-namespace")) + }) + + It("does not mutate unrelated Kafka fields", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + Kafka: apiv2.KafkaSpec{ManagedKafka: &apiv2.ManagedKafkaSpec{StorageSize: "20Gi", Replicas: 5}}, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.Kafka.ManagedKafka.StorageSize).To(g.Equal("20Gi")) + g.Expect(wandb.Spec.Kafka.ManagedKafka.Replicas).To(g.Equal(int32(5))) + }) + + It("creates managed Kafka defaults when Kafka is unset", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{}, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.Kafka.ManagedKafka).ToNot(g.BeNil()) + g.Expect(wandb.Spec.Kafka.ManagedKafka.Name).To(g.Equal("test-wandb-kafka")) + g.Expect(wandb.Spec.Kafka.ManagedKafka.Namespace).To(g.Equal("test-namespace")) + }) +}) diff --git a/internal/webhook/v2/weightsandbiases_defaulter_mysql_test.go b/internal/webhook/v2/weightsandbiases_defaulter_mysql_test.go new file mode 100644 index 00000000..4e208878 --- /dev/null +++ b/internal/webhook/v2/weightsandbiases_defaulter_mysql_test.go @@ -0,0 +1,80 @@ +package v2 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + g "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("WeightsAndBiasesCustomDefaulter - MySQL", func() { + var ( + ctx context.Context + defaulter WeightsAndBiasesCustomDefaulter + ) + + BeforeEach(func() { + ctx = context.Background() + defaulter = WeightsAndBiasesCustomDefaulter{} + }) + + It("defaults MySQL namespace", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{MySQL: map[string]apiv2.MySQLSpec{apiv2.DefaultInstanceName: {ManagedMysql: &apiv2.ManagedMysqlSpec{}}}}, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.MySQL[apiv2.DefaultInstanceName].ManagedMysql.Namespace).To(g.Equal("test-namespace")) + }) + + It("preserves custom MySQL namespace", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + MySQL: map[string]apiv2.MySQLSpec{ + apiv2.DefaultInstanceName: { + ManagedMysql: &apiv2.ManagedMysqlSpec{ + Namespace: "custom-moco-namespace", + }, + }, + }, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.MySQL[apiv2.DefaultInstanceName].ManagedMysql.Namespace).To(g.Equal("custom-moco-namespace")) + }) + + It("does not mutate unrelated MySQL fields", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + MySQL: map[string]apiv2.MySQLSpec{apiv2.DefaultInstanceName: {ManagedMysql: &apiv2.ManagedMysqlSpec{StorageSize: "50Gi"}}}, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.MySQL[apiv2.DefaultInstanceName].ManagedMysql.StorageSize).To(g.Equal("50Gi")) + }) + + It("does not apply defaults when ExternalMysql is present", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + MySQL: map[string]apiv2.MySQLSpec{ + apiv2.DefaultInstanceName: {ExternalMysql: &apiv2.MysqlConnection{}}, + }, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.MySQL[apiv2.DefaultInstanceName].ManagedMysql).To(g.BeNil()) + }) +}) diff --git a/internal/webhook/v2/weightsandbiases_defaulter_objectstore_test.go b/internal/webhook/v2/weightsandbiases_defaulter_objectstore_test.go new file mode 100644 index 00000000..ad96f480 --- /dev/null +++ b/internal/webhook/v2/weightsandbiases_defaulter_objectstore_test.go @@ -0,0 +1,84 @@ +package v2 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + g "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("WeightsAndBiasesCustomDefaulter - ObjectStore", func() { + var ( + ctx context.Context + defaulter WeightsAndBiasesCustomDefaulter + ) + + BeforeEach(func() { + ctx = context.Background() + defaulter = WeightsAndBiasesCustomDefaulter{} + }) + + It("defaults ObjectStore namespace to the parent namespace", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ObjectStore: map[string]apiv2.ObjectStoreSpec{apiv2.DefaultInstanceName: {ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{}}}}, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.Namespace).To(g.Equal("test-namespace")) + }) + + It("preserves a custom ObjectStore namespace", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + ObjectStore: map[string]apiv2.ObjectStoreSpec{apiv2.DefaultInstanceName: {ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{Namespace: "custom-objectstore-namespace"}}}, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.Namespace).To(g.Equal("custom-objectstore-namespace")) + }) + + It("does not mutate unrelated ObjectStore fields", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + ObjectStore: map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: { + ManagedObjectStore: &apiv2.ManagedObjectStoreSpec{ + StorageSize: "50Gi", + Replicas: 4, + Config: apiv2.ObjectStoreConfig{AccessKey: "custom-admin"}, + }, + }, + }, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.StorageSize).To(g.Equal("50Gi")) + g.Expect(wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.Replicas).To(g.Equal(int32(4))) + g.Expect(wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore.Config.AccessKey).To(g.Equal("custom-admin")) + }) + + It("does not apply defaults when ExternalObjectStore is present", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + ObjectStore: map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: {ExternalObjectStore: &apiv2.ObjectStoreConnection{}}, + }, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore).To(g.BeNil()) + }) +}) diff --git a/internal/webhook/v2/weightsandbiases_defaulter_redis_test.go b/internal/webhook/v2/weightsandbiases_defaulter_redis_test.go new file mode 100644 index 00000000..45eb16b9 --- /dev/null +++ b/internal/webhook/v2/weightsandbiases_defaulter_redis_test.go @@ -0,0 +1,75 @@ +package v2 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + g "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("WeightsAndBiasesCustomDefaulter - Redis", func() { + var ( + ctx context.Context + defaulter WeightsAndBiasesCustomDefaulter + ) + + BeforeEach(func() { + ctx = context.Background() + defaulter = WeightsAndBiasesCustomDefaulter{} + }) + + It("defaults Redis namespace to the parent namespace", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{Redis: map[string]apiv2.RedisSpec{apiv2.DefaultInstanceName: {ManagedRedis: &apiv2.ManagedRedisSpec{}}}}, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.Redis[apiv2.DefaultInstanceName].ManagedRedis.Namespace).To(g.Equal("test-namespace")) + }) + + It("preserves a custom Redis namespace", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + Redis: map[string]apiv2.RedisSpec{apiv2.DefaultInstanceName: {ManagedRedis: &apiv2.ManagedRedisSpec{Namespace: "custom-redis-namespace"}}}, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.Redis[apiv2.DefaultInstanceName].ManagedRedis.Namespace).To(g.Equal("custom-redis-namespace")) + }) + + It("does not mutate unrelated Redis fields", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + Redis: map[string]apiv2.RedisSpec{apiv2.DefaultInstanceName: {ManagedRedis: &apiv2.ManagedRedisSpec{StorageSize: "20Gi", Sentinel: apiv2.RedisSentinelSpec{Enabled: true}}}}, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.Redis[apiv2.DefaultInstanceName].ManagedRedis.StorageSize).To(g.Equal("20Gi")) + g.Expect(wandb.Spec.Redis[apiv2.DefaultInstanceName].ManagedRedis.Sentinel.Enabled).To(g.BeTrue()) + }) + + It("does not apply defaults when External is present", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: apiv2.WeightsAndBiasesSpec{ + Redis: map[string]apiv2.RedisSpec{ + apiv2.DefaultInstanceName: {ExternalRedis: &apiv2.RedisConnection{}}, + }, + }, + } + + err := defaulter.Default(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + g.Expect(wandb.Spec.Redis[apiv2.DefaultInstanceName].ManagedRedis).To(g.BeNil()) + }) +}) diff --git a/internal/webhook/v2/weightsandbiases_multi_instance_test.go b/internal/webhook/v2/weightsandbiases_multi_instance_test.go new file mode 100644 index 00000000..0934b6b2 --- /dev/null +++ b/internal/webhook/v2/weightsandbiases_multi_instance_test.go @@ -0,0 +1,85 @@ +package v2 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + g "github.com/onsi/gomega" + apiv2 "github.com/wandb/operator/api/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("WeightsAndBiases multi-instance infra", func() { + var ( + ctx context.Context + defaulter WeightsAndBiasesCustomDefaulter + validator WeightsAndBiasesCustomValidator + ) + + BeforeEach(func() { + ctx = context.Background() + defaulter = WeightsAndBiasesCustomDefaulter{} + validator = WeightsAndBiasesCustomValidator{} + }) + + It("seeds a managed default instance when the map is empty", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wb", Namespace: "ns"}, + } + + g.Expect(defaulter.Default(ctx, wandb)).To(g.Succeed()) + + mysql, ok := wandb.Spec.MySQL[apiv2.DefaultInstanceName] + g.Expect(ok).To(g.BeTrue()) + g.Expect(mysql.ManagedMysql).ToNot(g.BeNil()) + g.Expect(mysql.ManagedMysql.Name).To(g.Equal("wb-mysql")) + }) + + It("names the default instance plainly and keys other instances before the suffix", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wb", Namespace: "ns"}, + Spec: apiv2.WeightsAndBiasesSpec{ + MySQL: map[string]apiv2.MySQLSpec{ + apiv2.DefaultInstanceName: {ManagedMysql: &apiv2.ManagedMysqlSpec{}}, + "analytics": {ManagedMysql: &apiv2.ManagedMysqlSpec{}}, + }, + }, + } + + g.Expect(defaulter.Default(ctx, wandb)).To(g.Succeed()) + + g.Expect(wandb.Spec.MySQL[apiv2.DefaultInstanceName].ManagedMysql.Name).To(g.Equal("wb-mysql")) + g.Expect(wandb.Spec.MySQL["analytics"].ManagedMysql.Name).To(g.Equal("wb-analytics-mysql")) + g.Expect(wandb.Spec.MySQL["analytics"].ManagedMysql.Namespace).To(g.Equal("ns")) + }) + + It("rejects instances defined without a default instance", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wb", Namespace: "ns"}, + Spec: apiv2.WeightsAndBiasesSpec{ + MySQL: map[string]apiv2.MySQLSpec{ + "analytics": {ManagedMysql: &apiv2.ManagedMysqlSpec{Name: "wb-mysql-analytics", Namespace: "ns"}}, + }, + }, + } + + _, err := validator.ValidateCreate(ctx, wandb) + g.Expect(err).To(g.HaveOccurred()) + g.Expect(err.Error()).To(g.ContainSubstring("default")) + }) + + It("accepts multiple instances when a default is present", func() { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wb", Namespace: "ns"}, + Spec: apiv2.WeightsAndBiasesSpec{ + MySQL: map[string]apiv2.MySQLSpec{ + apiv2.DefaultInstanceName: {ManagedMysql: &apiv2.ManagedMysqlSpec{Name: "wb-mysql", Namespace: "ns"}}, + "analytics": {ManagedMysql: &apiv2.ManagedMysqlSpec{Name: "wb-mysql-analytics", Namespace: "ns"}}, + }, + }, + } + + _, err := validator.ValidateCreate(ctx, wandb) + g.Expect(err).ToNot(g.HaveOccurred()) + }) +}) diff --git a/internal/webhook/v2/weightsandbiases_probe_defaults.go b/internal/webhook/v2/weightsandbiases_probe_defaults.go new file mode 100644 index 00000000..d22031c0 --- /dev/null +++ b/internal/webhook/v2/weightsandbiases_probe_defaults.go @@ -0,0 +1,52 @@ +package v2 + +import ( + appsv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/probes" + corev1 "k8s.io/api/core/v1" +) + +const ( + defaultStartupProbePeriodSeconds int32 = 5 + defaultStartupProbeTimeoutSeconds int32 = 2 + defaultStartupProbeFailureThreshold int32 = 24 + defaultStartupProbeSuccessThreshold int32 = 1 + + defaultProbePeriodSeconds int32 = 10 + defaultProbeTimeoutSeconds int32 = 1 + defaultProbeFailureThreshold int32 = 3 + defaultProbeSuccessThreshold int32 = 1 +) + +func applyProbeDefaults(wandb *appsv2.WeightsAndBiases) { + wandb.Spec.Wandb.Probes.StartupProbe = probes.ApplyTemplate( + wandb.Spec.Wandb.Probes.StartupProbe, + defaultStartupProbeTemplate(), + ) + wandb.Spec.Wandb.Probes.LivenessProbe = probes.ApplyTemplate( + wandb.Spec.Wandb.Probes.LivenessProbe, + defaultProbeTemplate(), + ) + wandb.Spec.Wandb.Probes.ReadinessProbe = probes.ApplyTemplate( + wandb.Spec.Wandb.Probes.ReadinessProbe, + defaultProbeTemplate(), + ) +} + +func defaultStartupProbeTemplate() *corev1.Probe { + return &corev1.Probe{ + PeriodSeconds: defaultStartupProbePeriodSeconds, + TimeoutSeconds: defaultStartupProbeTimeoutSeconds, + FailureThreshold: defaultStartupProbeFailureThreshold, + SuccessThreshold: defaultStartupProbeSuccessThreshold, + } +} + +func defaultProbeTemplate() *corev1.Probe { + return &corev1.Probe{ + PeriodSeconds: defaultProbePeriodSeconds, + TimeoutSeconds: defaultProbeTimeoutSeconds, + FailureThreshold: defaultProbeFailureThreshold, + SuccessThreshold: defaultProbeSuccessThreshold, + } +} diff --git a/internal/webhook/v2/weightsandbiases_probe_defaults_test.go b/internal/webhook/v2/weightsandbiases_probe_defaults_test.go new file mode 100644 index 00000000..f90b7137 --- /dev/null +++ b/internal/webhook/v2/weightsandbiases_probe_defaults_test.go @@ -0,0 +1,202 @@ +package v2 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv2 "github.com/wandb/operator/api/v2" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +var _ = Describe("WeightsAndBiasesCustomDefaulter - Probe defaults", func() { + var ( + ctx context.Context + defaulter WeightsAndBiasesCustomDefaulter + ) + + BeforeEach(func() { + ctx = context.Background() + defaulter = WeightsAndBiasesCustomDefaulter{} + }) + + It("defaults probe timing templates on the W&B CR", func() { + wandb := &appsv2.WeightsAndBiases{ + ObjectMeta: v1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + } + + Expect(defaulter.Default(ctx, wandb)).To(Succeed()) + + expectProbeDefaults( + wandb.Spec.Wandb.Probes.StartupProbe, + defaultStartupProbePeriodSeconds, + defaultStartupProbeTimeoutSeconds, + defaultStartupProbeFailureThreshold, + defaultStartupProbeSuccessThreshold, + ) + expectProbeDefaults( + wandb.Spec.Wandb.Probes.LivenessProbe, + defaultProbePeriodSeconds, + defaultProbeTimeoutSeconds, + defaultProbeFailureThreshold, + defaultProbeSuccessThreshold, + ) + expectProbeDefaults( + wandb.Spec.Wandb.Probes.ReadinessProbe, + defaultProbePeriodSeconds, + defaultProbeTimeoutSeconds, + defaultProbeFailureThreshold, + defaultProbeSuccessThreshold, + ) + }) + + It("preserves partial probe handlers while filling missing timing defaults", func() { + wandb := &appsv2.WeightsAndBiases{ + ObjectMeta: v1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: appsv2.WeightsAndBiasesSpec{ + Wandb: appsv2.WandbAppSpec{ + Probes: appsv2.WandbProbeDefaults{ + StartupProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/startup"}, + }, + }, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/healthz", + Port: intstr.FromInt(8080), + }, + }, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/ready", + Scheme: corev1.URISchemeHTTPS, + }, + }, + }, + }, + }, + }, + } + + Expect(defaulter.Default(ctx, wandb)).To(Succeed()) + + startupProbe := wandb.Spec.Wandb.Probes.StartupProbe + Expect(startupProbe).ToNot(BeNil()) + Expect(startupProbe.HTTPGet).ToNot(BeNil()) + Expect(startupProbe.HTTPGet.Path).To(Equal("/startup")) + Expect(startupProbe.HTTPGet.Port).To(Equal(intstr.IntOrString{})) + Expect(startupProbe.PeriodSeconds).To(Equal(defaultStartupProbePeriodSeconds)) + Expect(startupProbe.TimeoutSeconds).To(Equal(defaultStartupProbeTimeoutSeconds)) + Expect(startupProbe.FailureThreshold).To(Equal(defaultStartupProbeFailureThreshold)) + + livenessProbe := wandb.Spec.Wandb.Probes.LivenessProbe + Expect(livenessProbe).ToNot(BeNil()) + Expect(livenessProbe.HTTPGet).ToNot(BeNil()) + Expect(livenessProbe.HTTPGet.Path).To(Equal("/healthz")) + Expect(livenessProbe.HTTPGet.Port).To(Equal(intstr.FromInt(8080))) + Expect(livenessProbe.PeriodSeconds).To(Equal(defaultProbePeriodSeconds)) + Expect(livenessProbe.TimeoutSeconds).To(Equal(defaultProbeTimeoutSeconds)) + Expect(livenessProbe.FailureThreshold).To(Equal(defaultProbeFailureThreshold)) + + readinessProbe := wandb.Spec.Wandb.Probes.ReadinessProbe + Expect(readinessProbe).ToNot(BeNil()) + Expect(readinessProbe.HTTPGet).ToNot(BeNil()) + Expect(readinessProbe.HTTPGet.Path).To(Equal("/ready")) + Expect(readinessProbe.HTTPGet.Scheme).To(Equal(corev1.URISchemeHTTPS)) + Expect(readinessProbe.PeriodSeconds).To(Equal(defaultProbePeriodSeconds)) + Expect(readinessProbe.TimeoutSeconds).To(Equal(defaultProbeTimeoutSeconds)) + Expect(readinessProbe.FailureThreshold).To(Equal(defaultProbeFailureThreshold)) + }) + + It("preserves explicit probe timing values", func() { + wandb := &appsv2.WeightsAndBiases{ + ObjectMeta: v1.ObjectMeta{Name: "test-wandb", Namespace: "test-namespace"}, + Spec: appsv2.WeightsAndBiasesSpec{ + Wandb: appsv2.WandbAppSpec{ + Probes: appsv2.WandbProbeDefaults{ + StartupProbe: &corev1.Probe{ + InitialDelaySeconds: 1, + PeriodSeconds: 3, + TimeoutSeconds: 7, + FailureThreshold: 60, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/healthz/initialized", + Port: intstr.FromInt(8080), + }, + }, + }, + LivenessProbe: &corev1.Probe{ + PeriodSeconds: 11, + TimeoutSeconds: 12, + SuccessThreshold: 1, + FailureThreshold: 13, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/healthz"}, + }, + }, + ReadinessProbe: &corev1.Probe{ + PeriodSeconds: 21, + TimeoutSeconds: 22, + SuccessThreshold: 2, + FailureThreshold: 23, + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{Path: "/ready"}, + }, + }, + }, + }, + }, + } + + Expect(defaulter.Default(ctx, wandb)).To(Succeed()) + + startupProbe := wandb.Spec.Wandb.Probes.StartupProbe + Expect(startupProbe).ToNot(BeNil()) + Expect(startupProbe.InitialDelaySeconds).To(Equal(int32(1))) + Expect(startupProbe.PeriodSeconds).To(Equal(int32(3))) + Expect(startupProbe.TimeoutSeconds).To(Equal(int32(7))) + Expect(startupProbe.FailureThreshold).To(Equal(int32(60))) + Expect(startupProbe.HTTPGet.Path).To(Equal("/healthz/initialized")) + Expect(startupProbe.HTTPGet.Port).To(Equal(intstr.FromInt(8080))) + + livenessProbe := wandb.Spec.Wandb.Probes.LivenessProbe + Expect(livenessProbe.PeriodSeconds).To(Equal(int32(11))) + Expect(livenessProbe.TimeoutSeconds).To(Equal(int32(12))) + Expect(livenessProbe.SuccessThreshold).To(Equal(int32(1))) + Expect(livenessProbe.FailureThreshold).To(Equal(int32(13))) + Expect(livenessProbe.HTTPGet.Path).To(Equal("/healthz")) + + readinessProbe := wandb.Spec.Wandb.Probes.ReadinessProbe + Expect(readinessProbe.PeriodSeconds).To(Equal(int32(21))) + Expect(readinessProbe.TimeoutSeconds).To(Equal(int32(22))) + Expect(readinessProbe.SuccessThreshold).To(Equal(int32(2))) + Expect(readinessProbe.FailureThreshold).To(Equal(int32(23))) + Expect(readinessProbe.HTTPGet.Path).To(Equal("/ready")) + }) +}) + +func expectProbeDefaults( + probe *corev1.Probe, + periodSeconds, + timeoutSeconds, + failureThreshold, + successThreshold int32, +) { + Expect(probe).ToNot(BeNil()) + Expect(probe.PeriodSeconds).To(Equal(periodSeconds)) + Expect(probe.TimeoutSeconds).To(Equal(timeoutSeconds)) + Expect(probe.FailureThreshold).To(Equal(failureThreshold)) + Expect(probe.SuccessThreshold).To(Equal(successThreshold)) + Expect(probe.InitialDelaySeconds).To(Equal(int32(0))) + Expect(probe.HTTPGet).To(BeNil()) + Expect(probe.Exec).To(BeNil()) + Expect(probe.TCPSocket).To(BeNil()) + Expect(probe.GRPC).To(BeNil()) +} diff --git a/internal/webhook/v2/weightsandbiases_webhook.go b/internal/webhook/v2/weightsandbiases_webhook.go new file mode 100644 index 00000000..0b1fd729 --- /dev/null +++ b/internal/webhook/v2/weightsandbiases_webhook.go @@ -0,0 +1,855 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + "context" + "fmt" + "strings" + + v1 "github.com/wandb/operator/api/v1" + "github.com/wandb/operator/internal/controller/infra/managed/clickhouse/altinity" + "github.com/wandb/operator/internal/controller/infra/managed/kafka/bufstream" + "github.com/wandb/operator/internal/controller/infra/managed/mysql/moco" + "github.com/wandb/operator/internal/controller/infra/managed/objectstore/seaweedfs" + "github.com/wandb/operator/internal/controller/infra/managed/redis/opstree" + "github.com/wandb/operator/internal/logx" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + appsv2 "github.com/wandb/operator/api/v2" +) + +// SetupWeightsAndBiasesWebhookWithManager registers the webhook for WeightsAndBiases in the manager. +func SetupWeightsAndBiasesWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&appsv2.WeightsAndBiases{}). + WithValidator(&WeightsAndBiasesCustomValidator{}). + WithDefaulter(&WeightsAndBiasesCustomDefaulter{}). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-apps-wandb-com-v2-weightsandbiases,mutating=true,failurePolicy=fail,sideEffects=None,groups=apps.wandb.com,resources=weightsandbiases,verbs=create;update,versions=v2,name=mweightsandbiases-v2.kb.io,admissionReviewVersions=v1 + +// WeightsAndBiasesCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind WeightsAndBiases when those are created or updated. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as it is used only for temporary operations and does not need to be deeply copied. +type WeightsAndBiasesCustomDefaulter struct { + // TODO(user): Add more fields as needed for defaulting +} + +var _ webhook.CustomDefaulter = &WeightsAndBiasesCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind WeightsAndBiases. +func (d *WeightsAndBiasesCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + _, log := logx.WithSlog(ctx, logx.DefaultingWebhook) + wandb, ok := obj.(*appsv2.WeightsAndBiases) + + if !ok { + return fmt.Errorf("expected an WeightsAndBiases object but got %T", obj) + } + log.Info("Defaulting for WeightsAndBiases", "name", wandb.GetName()) + + if wandb.Spec.Size == "" { + wandb.Spec.Size = appsv2.SizeDev + } + + if wandb.Spec.RetentionPolicy.OnDelete == "" { + wandb.Spec.RetentionPolicy.OnDelete = appsv2.DetachOnDelete + } + + if wandb.Spec.Affinity == nil { + wandb.Spec.Affinity = &corev1.Affinity{} + } + + if wandb.Spec.Tolerations == nil { + wandb.Spec.Tolerations = &[]corev1.Toleration{} + } + + if wandb.Spec.Wandb.Features == nil { + wandb.Spec.Wandb.Features = make(map[string]bool) + } + + if wandb.Spec.Wandb.ManifestRepository == "" { + wandb.Spec.Wandb.ManifestRepository = appsv2.DefaultManifestRepository + } + + if !strings.Contains(wandb.Spec.Wandb.ManifestRepository, "://") { + // Prepend a default scheme (e.g., oci://) to ensure proper parsing of the host. + wandb.Spec.Wandb.ManifestRepository = "oci://" + wandb.Spec.Wandb.ManifestRepository + } + + if wandb.Spec.Wandb.InternalServiceAuth.Enabled == nil { + wandb.Spec.Wandb.InternalServiceAuth.Enabled = ptr.To(true) + } + + if wandb.Spec.Wandb.InternalServiceAuth.OIDCIssuer == "" { + wandb.Spec.Wandb.InternalServiceAuth.OIDCIssuer = "https://kubernetes.default.svc.cluster.local" + } + + if wandb.Spec.Wandb.ServiceAccount.Create == nil { + wandb.Spec.Wandb.ServiceAccount.Create = ptr.To(true) + } + + if wandb.Spec.Wandb.ServiceAccount.ServiceAccountName == "" { + wandb.Spec.Wandb.ServiceAccount.ServiceAccountName = "wandb-app" + } + + if wandb.Status.Wandb.Applications == nil { + wandb.Status.Wandb.Applications = make(map[string]appsv2.ApplicationStatus) + } + + applyMySQLDefaults(wandb) + applyRedisDefaults(wandb) + applyKafkaDefaults(wandb) + applyObjectStoreDefaults(wandb) + applyClickHouseDefaults(wandb) + applyProbeDefaults(wandb) + + if defaultStore, ok := wandb.Spec.ObjectStore["default"]; ok && defaultStore.ManagedObjectStore != nil { + wandb.Spec.Wandb.BucketProxy = true + } + + return nil +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'. +// +kubebuilder:webhook:path=/validate-apps-wandb-com-v2-weightsandbiases,mutating=false,failurePolicy=fail,sideEffects=None,groups=apps.wandb.com,resources=weightsandbiases,verbs=create;update,versions=v2,name=vweightsandbiases-v2.kb.io,admissionReviewVersions=v1 + +// WeightsAndBiasesCustomValidator struct is responsible for validating the WeightsAndBiases resource +// when it is created, updated, or deleted. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as this struct is used only for temporary operations and does not need to be deeply copied. +type WeightsAndBiasesCustomValidator struct { + // TODO(user): Add more fields as needed for validation +} + +var _ webhook.CustomValidator = &WeightsAndBiasesCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type WeightsAndBiases. +func (v *WeightsAndBiasesCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + ctx, log := logx.WithSlog(ctx, logx.ValidatingWebhook) + wandb, ok := obj.(*appsv2.WeightsAndBiases) + if !ok { + return nil, fmt.Errorf("expected a WeightsAndBiases object but got %T", obj) + } + log.Info("Validation for WeightsAndBiases upon creation", "name", wandb.GetName()) + + return validateSpec(ctx, wandb, nil) +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type WeightsAndBiases. +func (v *WeightsAndBiasesCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + ctx, log := logx.WithSlog(ctx, logx.ValidatingWebhook) + newWandb, ok := newObj.(*appsv2.WeightsAndBiases) + if !ok { + return nil, fmt.Errorf("expected a WeightsAndBiases object for the newObj but got %T", newObj) + } + oldWandb, ok := oldObj.(*appsv2.WeightsAndBiases) + if !ok { + return nil, fmt.Errorf("expected a WeightsAndBiases object for the oldObj but got %T", oldObj) + } + log.Info("Validation for WeightsAndBiases upon update", "name", newWandb.GetName()) + + var specWarnings, changeWarnings admission.Warnings + var err error + + log.Info("validate V2 update", "name", newWandb.Name) + + if specWarnings, err = validateSpec(ctx, newWandb, oldWandb); err != nil { + return specWarnings, err + } + changeWarnings, err = validateChanges(ctx, newWandb, oldWandb) + return append(specWarnings, changeWarnings...), err +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type WeightsAndBiases. +func (v *WeightsAndBiasesCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + _, log := logx.WithSlog(ctx, logx.ValidatingWebhook) + weightsandbiases, ok := obj.(*appsv2.WeightsAndBiases) + if !ok { + return nil, fmt.Errorf("expected a WeightsAndBiases object but got %T", obj) + } + log.Info("Validation for WeightsAndBiases upon deletion", "name", weightsandbiases.GetName()) + + // TODO(user): fill in your validation logic upon object deletion. + + return nil, nil +} + +func applyMySQLDefaults(wandb *appsv2.WeightsAndBiases) { + if wandb.Spec.MySQL == nil { + wandb.Spec.MySQL = map[string]appsv2.MySQLSpec{} + } + if len(wandb.Spec.MySQL) == 0 { + wandb.Spec.MySQL[appsv2.DefaultInstanceName] = appsv2.MySQLSpec{ManagedMysql: &appsv2.ManagedMysqlSpec{}} + } + + for key, spec := range wandb.Spec.MySQL { + if spec.ExternalMysql != nil { + continue + } + if spec.ManagedMysql == nil { + spec.ManagedMysql = &appsv2.ManagedMysqlSpec{} + } + if spec.ManagedMysql.Name == "" { + spec.ManagedMysql.Name = moco.DefaultSpecName(wandb.Name, key) + } + if spec.ManagedMysql.Namespace == "" { + spec.ManagedMysql.Namespace = wandb.Namespace + } + wandb.Spec.MySQL[key] = spec + } +} + +func applyRedisDefaults(wandb *appsv2.WeightsAndBiases) { + if wandb.Spec.Redis == nil { + wandb.Spec.Redis = map[string]appsv2.RedisSpec{} + } + if len(wandb.Spec.Redis) == 0 { + wandb.Spec.Redis[appsv2.DefaultInstanceName] = appsv2.RedisSpec{ManagedRedis: &appsv2.ManagedRedisSpec{}} + } + + for key, spec := range wandb.Spec.Redis { + if spec.ExternalRedis != nil { + continue + } + if spec.ManagedRedis == nil { + spec.ManagedRedis = &appsv2.ManagedRedisSpec{} + } + if spec.ManagedRedis.Name == "" { + spec.ManagedRedis.Name = opstree.DefaultSpecName(wandb.Name, key) + } + if spec.ManagedRedis.Namespace == "" { + spec.ManagedRedis.Namespace = wandb.Namespace + } + if wandb.Spec.Size != appsv2.SizeDev { + spec.ManagedRedis.Sentinel.Enabled = true + } + wandb.Spec.Redis[key] = spec + } +} + +func applyKafkaDefaults(wandb *appsv2.WeightsAndBiases) { + if wandb.Spec.Kafka.ManagedKafka == nil { + wandb.Spec.Kafka.ManagedKafka = &appsv2.ManagedKafkaSpec{} + } + + spec := wandb.Spec.Kafka.ManagedKafka + + if spec.Name == "" { + spec.Name = bufstream.DefaultSpecName(wandb.Name, appsv2.DefaultInstanceName) + } + + if spec.Namespace == "" { + spec.Namespace = wandb.Namespace + } +} + +func applyObjectStoreDefaults(wandb *appsv2.WeightsAndBiases) { + if wandb.Spec.ObjectStore == nil { + wandb.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{} + } + if len(wandb.Spec.ObjectStore) == 0 { + wandb.Spec.ObjectStore[appsv2.DefaultInstanceName] = appsv2.ObjectStoreSpec{ManagedObjectStore: &appsv2.ManagedObjectStoreSpec{}} + } + + for key, spec := range wandb.Spec.ObjectStore { + if spec.ExternalObjectStore != nil { + continue + } + if spec.ManagedObjectStore == nil { + spec.ManagedObjectStore = &appsv2.ManagedObjectStoreSpec{} + } + managed := spec.ManagedObjectStore + if managed.Name == "" { + managed.Name = seaweedfs.DefaultSpecName(wandb.Name, key) + } + if managed.Namespace == "" { + managed.Namespace = wandb.Namespace + } + if managed.Config.AccessKey == "" && managed.Config.RootUser != "" { //nolint:staticcheck + managed.Config.AccessKey = managed.Config.RootUser //nolint:staticcheck + } + if managed.Config.AccessKey == "" { + managed.Config.AccessKey = "admin" + } + wandb.Spec.ObjectStore[key] = spec + } +} + +func applyClickHouseDefaults(wandb *appsv2.WeightsAndBiases) { + if wandb.Spec.ClickHouse == nil { + wandb.Spec.ClickHouse = map[string]appsv2.ClickHouseSpec{} + } + if len(wandb.Spec.ClickHouse) == 0 { + wandb.Spec.ClickHouse[appsv2.DefaultInstanceName] = appsv2.ClickHouseSpec{ManagedClickHouse: &appsv2.ManagedClickHouseSpec{}} + } + + for key, spec := range wandb.Spec.ClickHouse { + if spec.ExternalClickHouse != nil { + continue + } + if spec.ManagedClickHouse == nil { + spec.ManagedClickHouse = &appsv2.ManagedClickHouseSpec{} + } + if spec.ManagedClickHouse.Name == "" { + spec.ManagedClickHouse.Name = altinity.DefaultSpecName(wandb.Name, key) + } + if spec.ManagedClickHouse.Namespace == "" { + spec.ManagedClickHouse.Namespace = wandb.Namespace + } + wandb.Spec.ClickHouse[key] = spec + } +} + +// validateSpec validates the (already defaulted) spec. oldWandb is nil on +// create; update rules use it to skip values unchanged from the stored object. +func validateSpec(_ context.Context, newWandb, oldWandb *appsv2.WeightsAndBiases) (admission.Warnings, error) { + var allErrors field.ErrorList + var warnings admission.Warnings + + allErrors = append(allErrors, validateMySQLSpec(newWandb)...) + allErrors = append(allErrors, validateRedisSpec(newWandb)...) + allErrors = append(allErrors, validateObjectStoreSpec(newWandb)...) + allErrors = append(allErrors, validateClickHouseSpec(newWandb)...) + allErrors = append(allErrors, validateInfraNames(newWandb, oldWandb)...) + networkingErrors, networkingWarnings := validateNetworkingSpec(newWandb) + allErrors = append(allErrors, networkingErrors...) + warnings = append(warnings, networkingWarnings...) + + if len(allErrors) == 0 { + return warnings, nil + } + + return warnings, apierrors.NewInvalid( + schema.GroupKind{Group: "apps.wandb.com", Kind: "WeightsAndBiases"}, + newWandb.Name, + allErrors, + ) +} + +func validateChanges(_ context.Context, newWandb *appsv2.WeightsAndBiases, oldWandb *appsv2.WeightsAndBiases) (admission.Warnings, error) { + var allErrors field.ErrorList + var warnings admission.Warnings + + allErrors = append(allErrors, validateRedisChanges(newWandb, oldWandb)...) + allErrors = append(allErrors, validateMySQLChanges(newWandb, oldWandb)...) + + if len(allErrors) == 0 { + return warnings, nil + } + + return warnings, apierrors.NewInvalid( + schema.GroupKind{Group: "apps.wandb.com", Kind: "WeightsAndBiases"}, + newWandb.Name, + allErrors, + ) +} + +// validateHasDefaultInstance reports an error when a multi-instance infra type +// defines at least one instance but is missing the reserved default key, which +// the env-var fallback relies on. +func validateHasDefaultInstance[T any](m map[string]T, path *field.Path) field.ErrorList { + if len(m) == 0 { + return nil + } + if _, ok := m[appsv2.DefaultInstanceName]; ok { + return nil + } + return field.ErrorList{field.Required( + path.Key(appsv2.DefaultInstanceName), + fmt.Sprintf("a %q instance is required when other instances are defined", appsv2.DefaultInstanceName), + )} +} + +// validateMySQLChanges rejects an update that lowers an explicitly-set replica +// count. Moco does not support in-place replica reduction, so catch it at +// admission for immediate feedback. A size-driven change leaves replicas unset +// here (it is resolved from the manifest at reconcile), so this only covers a +// directly-edited count. +func validateMySQLChanges(newWandb, oldWandb *appsv2.WeightsAndBiases) field.ErrorList { + var errors field.ErrorList + mysqlPath := field.NewPath("spec").Child("mysql") + + for key, newInstance := range newWandb.Spec.MySQL { + oldInstance, ok := oldWandb.Spec.MySQL[key] + if !ok { + continue + } + newSpec := newInstance.ManagedMysql + oldSpec := oldInstance.ManagedMysql + if newSpec == nil || oldSpec == nil { + continue + } + + if oldSpec.Replicas != 0 && newSpec.Replicas != 0 && newSpec.Replicas < oldSpec.Replicas { + errors = append(errors, field.Invalid( + mysqlPath.Key(key).Child("managedMysql").Child("replicas"), + newSpec.Replicas, + "replicas cannot be decreased; Moco does not support in-place replica reduction (use its manual stop-clustering procedure)", + )) + } + } + + return errors +} + +func validateMySQLSpec(wandb *appsv2.WeightsAndBiases) field.ErrorList { + var errors field.ErrorList + mysqlPath := field.NewPath("spec").Child("mysql") + + errors = append(errors, validateHasDefaultInstance(wandb.Spec.MySQL, mysqlPath)...) + + for key, spec := range wandb.Spec.MySQL { + instancePath := mysqlPath.Key(key) + if spec.ManagedMysql != nil && spec.ExternalMysql != nil { + errors = append(errors, field.Invalid( + instancePath, + "", + "managedMysql and externalMysql are mutually exclusive", + )) + } + if managed := spec.ManagedMysql; managed != nil { + if managed.Replicas != 0 && !appsv2.ValidMysqlReplicaCount(managed.Replicas) { + errors = append(errors, field.Invalid( + instancePath.Child("managedMysql").Child("replicas"), + managed.Replicas, + "replicas must be an odd number (Moco enforces quorum-based replication)", + )) + } + } + } + + return errors +} + +func validateRedisSpec(wandb *appsv2.WeightsAndBiases) field.ErrorList { + var errors field.ErrorList + redisPath := field.NewPath("spec").Child("redis") + + errors = append(errors, validateHasDefaultInstance(wandb.Spec.Redis, redisPath)...) + + for key, spec := range wandb.Spec.Redis { + instancePath := redisPath.Key(key) + if spec.ManagedRedis != nil && spec.ExternalRedis != nil { + errors = append(errors, field.Invalid( + instancePath, + "", + "managedRedis and externalRedis are mutually exclusive", + )) + } + + managed := spec.ManagedRedis + if managed == nil { + continue + } + + if managed.StorageSize != "" { + if _, err := resource.ParseQuantity(managed.StorageSize); err != nil { + errors = append(errors, field.Invalid( + instancePath.Child("managedRedis").Child("storageSize"), + managed.StorageSize, + "must be a valid resource quantity (e.g., '10Gi')", + )) + } + } + } + + return errors +} + +func validateObjectStoreSpec(wandb *appsv2.WeightsAndBiases) field.ErrorList { + var errors field.ErrorList + objectStorePath := field.NewPath("spec").Child("objectStore") + + errors = append(errors, validateHasDefaultInstance(wandb.Spec.ObjectStore, objectStorePath)...) + + for key, spec := range wandb.Spec.ObjectStore { + if spec.ManagedObjectStore != nil && spec.ExternalObjectStore != nil { + errors = append(errors, field.Invalid( + objectStorePath.Key(key), + "", + "managedObjectStore and externalObjectStore are mutually exclusive", + )) + } + + if mgd := spec.ManagedObjectStore; mgd != nil && mgd.StorageSize != "" { + // Reject non-positive too: "0"/"-5Gi" parse fine but only fail later at PVC creation. + if q, err := resource.ParseQuantity(mgd.StorageSize); err != nil || q.Sign() <= 0 { + errors = append(errors, field.Invalid( + objectStorePath.Key(key).Child("managedObjectStore").Child("storageSize"), + mgd.StorageSize, + "must be a positive resource quantity (e.g., '10Gi')", + )) + } + } + + if mgd := spec.ManagedObjectStore; mgd != nil && mgd.SeaweedObjectStoreSpec.FilerStorageSize != "" { + if q, err := resource.ParseQuantity(mgd.SeaweedObjectStoreSpec.FilerStorageSize); err != nil || q.Sign() <= 0 { + errors = append(errors, field.Invalid( + objectStorePath.Key(key).Child("managedObjectStore").Child("SeaweedObjectStoreSpec").Child("filerStorageSize"), + mgd.SeaweedObjectStoreSpec.FilerStorageSize, + "must be a positive resource quantity (e.g., '10Gi')", + )) + } + } + + if mgd := spec.ManagedObjectStore; mgd != nil { + // Only check the copies/replicas relationship when the user pinned replicas; + // otherwise the manifest supplies it at reconcile and seaweedReplication clamps it. + if mgd.Copies < 0 || (mgd.Replicas > 0 && mgd.Copies > mgd.Replicas-1) { + errors = append(errors, field.Invalid( + objectStorePath.Key(key).Child("managedObjectStore").Child("copies"), + mgd.Copies, + "copies cannot be negative or exceed replicas-1 (one copy per other data node)", + )) + } + } + + if ext := spec.ExternalObjectStore; ext != nil { + extPath := objectStorePath.Key(key).Child("externalObjectStore") + // provider is sourced from a secret key, so it is resolved and defaulted at reconcile time, not here. + if _, ok := wandb.GetAnnotations()[v1.BucketPendingAnnotation]; !ok && ext.Bucket.Name == "" { + errors = append(errors, field.Required( + extPath.Child("bucket"), + "externalObjectStore requires a bucket secret reference", + )) + } + } + } + + return errors +} + +func validateClickHouseSpec(wandb *appsv2.WeightsAndBiases) field.ErrorList { + var errors field.ErrorList + chPath := field.NewPath("spec").Child("clickhouse") + + errors = append(errors, validateHasDefaultInstance(wandb.Spec.ClickHouse, chPath)...) + + for key, spec := range wandb.Spec.ClickHouse { + instancePath := chPath.Key(key) + if spec.ManagedClickHouse != nil && spec.ExternalClickHouse != nil { + errors = append(errors, field.Invalid( + instancePath, + "", + "managedClickhouse and externalClickhouse are mutually exclusive", + )) + } + + managed := spec.ManagedClickHouse + if managed == nil { + continue + } + + // Managed ClickHouse stores table data in the object store, so one must be configured. + if len(wandb.Spec.ObjectStore) == 0 { + errors = append(errors, field.Invalid( + instancePath.Child("managedClickhouse"), + "", + "managed ClickHouse stores data in the object store; configure spec.objectStore (managed or external)", + )) + } + + // Keeper requires an odd number of replicas to form a quorum. + if managed.Keeper.Replicas != 0 && managed.Keeper.Replicas%2 == 0 { + errors = append(errors, field.Invalid( + instancePath.Child("managedClickhouse").Child("keeper").Child("replicas"), + managed.Keeper.Replicas, + "replicas must be an odd number so the Keeper ensemble can form a quorum", + )) + } + + for _, sz := range []struct { + value string + path *field.Path + }{ + {managed.StorageSize, instancePath.Child("managedClickhouse").Child("storageSize")}, + {managed.Keeper.StorageSize, instancePath.Child("managedClickhouse").Child("keeper").Child("storageSize")}, + } { + if sz.value == "" { + continue + } + if _, err := resource.ParseQuantity(sz.value); err != nil { + errors = append(errors, field.Invalid(sz.path, sz.value, "must be a valid resource quantity (e.g., '10Gi')")) + } + } + } + + return errors +} + +// validateInfraNames rejects managed infra names whose derived object names +// cannot be deployed (vendor operators wedge silently past DNS-1123 limits). +// Empty names are the defaulter's to fill; on update only changed names are +// checked (per instance key), so pre-existing CRs stay updatable and deletable. +func validateInfraNames(newWandb, oldWandb *appsv2.WeightsAndBiases) field.ErrorList { + var errors field.ErrorList + + changed := func(oldName, name string) bool { + if name == "" { + return false + } + return oldWandb == nil || oldName != name + } + + for key, spec := range newWandb.Spec.ClickHouse { + managed := spec.ManagedClickHouse + if managed == nil { + continue + } + oldName := "" + if oldWandb != nil { + if old, ok := oldWandb.Spec.ClickHouse[key]; ok && old.ManagedClickHouse != nil { + oldName = old.ManagedClickHouse.Name + } + } + if changed(oldName, managed.Name) { + if err := altinity.ValidateDerivedNames(managed); err != nil { + errors = append(errors, field.Invalid( + field.NewPath("spec").Child("clickhouse").Key(key).Child("managedClickhouse").Child("name"), + managed.Name, err.Error(), + )) + } + } + } + + for key, spec := range newWandb.Spec.MySQL { + managed := spec.ManagedMysql + if managed == nil { + continue + } + oldName := "" + if oldWandb != nil { + if old, ok := oldWandb.Spec.MySQL[key]; ok && old.ManagedMysql != nil { + oldName = old.ManagedMysql.Name + } + } + if changed(oldName, managed.Name) { + errors = append(errors, validateInfraName( + field.NewPath("spec").Child("mysql").Key(key).Child("managedMysql").Child("name"), + managed.Name, moco.MaxClusterNameLength, + "Moco rejects MySQLCluster names longer than 40 characters", + )...) + } + } + + for key, spec := range newWandb.Spec.Redis { + managed := spec.ManagedRedis + if managed == nil { + continue + } + oldName := "" + if oldWandb != nil { + if old, ok := oldWandb.Spec.Redis[key]; ok && old.ManagedRedis != nil { + oldName = old.ManagedRedis.Name + } + } + if changed(oldName, managed.Name) { + errors = append(errors, validateInfraName( + field.NewPath("spec").Child("redis").Key(key).Child("managedRedis").Child("name"), + managed.Name, opstree.MaxSpecNameLength, + "derived Redis workload and Service names must fit 63-character DNS-1123 labels", + )...) + } + } + + if spec := newWandb.Spec.Kafka.ManagedKafka; spec != nil { + oldName := "" + if oldWandb != nil && oldWandb.Spec.Kafka.ManagedKafka != nil { + oldName = oldWandb.Spec.Kafka.ManagedKafka.Name + } + if changed(oldName, spec.Name) { + errors = append(errors, validateInfraName( + field.NewPath("spec").Child("kafka").Child("managedKafka").Child("name"), + spec.Name, bufstream.MaxSpecNameLength(), + "derived Kafka/etcd pod names must fit 63-character DNS-1123 labels", + )...) + } + } + + for key, spec := range newWandb.Spec.ObjectStore { + managed := spec.ManagedObjectStore + if managed == nil { + continue + } + oldName := "" + if oldWandb != nil { + if old, ok := oldWandb.Spec.ObjectStore[key]; ok && old.ManagedObjectStore != nil { + oldName = old.ManagedObjectStore.Name + } + } + if changed(oldName, managed.Name) { + errors = append(errors, validateInfraName( + field.NewPath("spec").Child("objectStore").Key(key).Child("managedObjectStore").Child("name"), + managed.Name, seaweedfs.MaxSpecNameLength, + "derived SeaweedFS workload and Service names must fit 63-character DNS-1123 labels", + )...) + } + } + + return errors +} + +func validateInfraName(path *field.Path, name string, budget int, why string) field.ErrorList { + var errors field.ErrorList + if labelErrs := validation.IsDNS1123Label(name); len(labelErrs) > 0 { + errors = append(errors, field.Invalid( + path, name, + fmt.Sprintf("must be a valid DNS-1123 label: %s", strings.Join(labelErrs, "; ")), + )) + } else if len(name) > budget { + errors = append(errors, field.Invalid( + path, name, + fmt.Sprintf("must be at most %d characters: %s", budget, why), + )) + } + return errors +} + +func validateRedisChanges(newWandb, oldWandb *appsv2.WeightsAndBiases) field.ErrorList { + var errors field.ErrorList + redisPath := field.NewPath("spec").Child("redis") + + for key, newInstance := range newWandb.Spec.Redis { + newSpec := newInstance.ManagedRedis + if newSpec == nil { + continue + } + oldInstance, ok := oldWandb.Spec.Redis[key] + if !ok || oldInstance.ManagedRedis == nil { + continue + } + oldSpec := oldInstance.ManagedRedis + instancePath := redisPath.Key(key).Child("managedRedis") + + if oldSpec.StorageSize != "" && + oldSpec.StorageSize != newSpec.StorageSize { + errors = append(errors, field.Invalid( + instancePath.Child("storageSize"), + newSpec.StorageSize, + "storageSize may not be changed", + )) + } + + if oldSpec.Namespace != newSpec.Namespace { + errors = append(errors, field.Invalid( + instancePath.Child("namespace"), + newSpec.Namespace, + "namespace may not be changed", + )) + } + + if oldSpec.Sentinel.Enabled != newSpec.Sentinel.Enabled { + errors = append(errors, field.Invalid( + instancePath.Child("sentinel").Child("enabled"), + newSpec.Sentinel.Enabled, + "Redis Sentinel cannot be toggled between enabled and disabled (yet)", + )) + } + } + + return errors +} + +func validateNetworkingSpec(wandb *appsv2.WeightsAndBiases) (field.ErrorList, admission.Warnings) { + var errors field.ErrorList + var warnings admission.Warnings + netPath := field.NewPath("spec").Child("networking") + spec := wandb.Spec.Networking + + if spec.Mode == appsv2.NetworkingModeNone { + return errors, warnings + } + + if spec.Mode == appsv2.NetworkingModeIngress && spec.GatewayAPI != nil { + errors = append(errors, field.Invalid( + netPath.Child("gatewayAPI"), + spec.GatewayAPI, + "gatewayAPI must not be set when mode is Ingress", + )) + } + + if spec.Mode == appsv2.NetworkingModeGatewayAPI && spec.Ingress != nil { + errors = append(errors, field.Invalid( + netPath.Child("ingress"), + spec.Ingress, + "ingress must not be set when mode is GatewayAPI", + )) + } + + if spec.Mode == appsv2.NetworkingModeGatewayAPI { + if spec.GatewayAPI == nil { + errors = append(errors, field.Required( + netPath.Child("gatewayAPI"), + "gatewayAPI is required when mode is GatewayAPI", + )) + } else { + gwPath := netPath.Child("gatewayAPI").Child("gateway") + gw := spec.GatewayAPI.Gateway + + if gw.Managed { + if gw.GatewayClassName == nil || *gw.GatewayClassName == "" { + errors = append(errors, field.Required( + gwPath.Child("gatewayClassName"), + "gatewayClassName is required when gateway.managed is true", + )) + } + if gw.GatewayRef != nil { + errors = append(errors, field.Invalid( + gwPath.Child("gatewayRef"), + gw.GatewayRef, + "gatewayRef must not be set when gateway.managed is true", + )) + } + } else { + if gw.GatewayRef == nil { + errors = append(errors, field.Required( + gwPath.Child("gatewayRef"), + "gatewayRef is required when gateway.managed is false", + )) + } else if gw.GatewayRef.Name == "" { + errors = append(errors, field.Required( + gwPath.Child("gatewayRef").Child("name"), + "gatewayRef.name is required", + )) + } + } + } + } + + if spec.TLS != nil && spec.TLS.CertManager != nil && spec.Mode == "" { + warnings = append(warnings, "networking.tls.certManager annotations are only applied when using Ingress or GatewayAPI") + } + + return errors, warnings +} diff --git a/internal/webhook/v2/weightsandbiases_webhook_test.go b/internal/webhook/v2/weightsandbiases_webhook_test.go new file mode 100644 index 00000000..93520967 --- /dev/null +++ b/internal/webhook/v2/weightsandbiases_webhook_test.go @@ -0,0 +1,340 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv2 "github.com/wandb/operator/api/v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("WeightsAndBiases Webhook", func() { + var ( + ctx context.Context + obj *appsv2.WeightsAndBiases + oldObj *appsv2.WeightsAndBiases + validator WeightsAndBiasesCustomValidator + defaulter WeightsAndBiasesCustomDefaulter + ) + + BeforeEach(func() { + ctx = context.Background() + obj = &appsv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "test-ns"}} + oldObj = &appsv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "test-ns"}} + validator = WeightsAndBiasesCustomValidator{} + defaulter = WeightsAndBiasesCustomDefaulter{} + }) + + Context("When creating WeightsAndBiases under Defaulting Webhook", func() { + It("sets webhook defaults and preserves user-provided values", func() { + obj.Spec.RetentionPolicy.OnDelete = "" + obj.Spec.Wandb.ManifestRepository = "example.com/wandb/server-manifest" + obj.Spec.MySQL = map[string]appsv2.MySQLSpec{appsv2.DefaultInstanceName: {ManagedMysql: &appsv2.ManagedMysqlSpec{}}} + obj.Spec.Redis = map[string]appsv2.RedisSpec{appsv2.DefaultInstanceName: {ManagedRedis: &appsv2.ManagedRedisSpec{}}} + obj.Spec.Kafka.ManagedKafka = &appsv2.ManagedKafkaSpec{} + obj.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{appsv2.DefaultInstanceName: {ManagedObjectStore: &appsv2.ManagedObjectStoreSpec{}}} + Expect(defaulter.Default(ctx, obj)).To(Succeed()) + + Expect(obj.Spec.Size).To(Equal(appsv2.SizeDev)) + Expect(obj.Spec.RetentionPolicy.OnDelete).To(Equal(appsv2.DetachOnDelete)) + Expect(obj.Spec.Affinity).ToNot(BeNil()) + Expect(obj.Spec.Tolerations).ToNot(BeNil()) + Expect(obj.Spec.Wandb.ManifestRepository).To(Equal("oci://example.com/wandb/server-manifest")) + Expect(obj.Spec.Wandb.InternalServiceAuth.Enabled).ToNot(BeNil()) + Expect(*obj.Spec.Wandb.InternalServiceAuth.Enabled).To(BeTrue()) + Expect(obj.Spec.Wandb.InternalServiceAuth.OIDCIssuer).To(Equal("https://kubernetes.default.svc.cluster.local")) + Expect(obj.Spec.Wandb.ServiceAccount.Create).ToNot(BeNil()) + Expect(*obj.Spec.Wandb.ServiceAccount.Create).To(BeTrue()) + Expect(obj.Spec.Wandb.ServiceAccount.ServiceAccountName).To(Equal("wandb-app")) + Expect(obj.Status.Wandb.Applications).ToNot(BeNil()) + Expect(obj.Spec.MySQL[appsv2.DefaultInstanceName].ManagedMysql.Namespace).To(Equal("test-ns")) + Expect(obj.Spec.Redis[appsv2.DefaultInstanceName].ManagedRedis.Namespace).To(Equal("test-ns")) + Expect(obj.Spec.Kafka.ManagedKafka.Namespace).To(Equal("test-ns")) + Expect(obj.Spec.ObjectStore[appsv2.DefaultInstanceName].ManagedObjectStore.Namespace).To(Equal("test-ns")) + }) + + It("does not override already set values", func() { + affinity := &corev1.Affinity{} + tolerations := &[]corev1.Toleration{{Key: "dedicated", Operator: corev1.TolerationOpExists}} + obj.Spec.Size = appsv2.SizeSmall + obj.Spec.RetentionPolicy.OnDelete = appsv2.PurgeOnDelete + obj.Spec.Affinity = affinity + obj.Spec.Tolerations = tolerations + obj.Spec.Wandb.ManifestRepository = "oci://custom/repo" + obj.Spec.Wandb.InternalServiceAuth.Enabled = boolPtr(false) + obj.Spec.Wandb.InternalServiceAuth.OIDCIssuer = "https://issuer.example.com" + obj.Spec.Wandb.ServiceAccount.Create = boolPtr(false) + obj.Spec.Wandb.ServiceAccount.ServiceAccountName = "custom-sa" + obj.Spec.MySQL = map[string]appsv2.MySQLSpec{appsv2.DefaultInstanceName: {ManagedMysql: &appsv2.ManagedMysqlSpec{ + Namespace: "custom-moco", + }}} + obj.Spec.Redis = map[string]appsv2.RedisSpec{appsv2.DefaultInstanceName: {ManagedRedis: &appsv2.ManagedRedisSpec{Namespace: "custom-redis"}}} + obj.Spec.Kafka.ManagedKafka = &appsv2.ManagedKafkaSpec{Namespace: "custom-kafka"} + obj.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{appsv2.DefaultInstanceName: {ManagedObjectStore: &appsv2.ManagedObjectStoreSpec{Namespace: "custom-objectstore"}}} + obj.Status.Wandb.Applications = map[string]appsv2.ApplicationStatus{"api": {}} + + Expect(defaulter.Default(ctx, obj)).To(Succeed()) + + Expect(obj.Spec.Size).To(Equal(appsv2.SizeSmall)) + Expect(obj.Spec.RetentionPolicy.OnDelete).To(Equal(appsv2.PurgeOnDelete)) + Expect(obj.Spec.Affinity).To(BeIdenticalTo(affinity)) + Expect(obj.Spec.Tolerations).To(BeIdenticalTo(tolerations)) + Expect(obj.Spec.Wandb.ManifestRepository).To(Equal("oci://custom/repo")) + Expect(*obj.Spec.Wandb.InternalServiceAuth.Enabled).To(BeFalse()) + Expect(obj.Spec.Wandb.InternalServiceAuth.OIDCIssuer).To(Equal("https://issuer.example.com")) + Expect(*obj.Spec.Wandb.ServiceAccount.Create).To(BeFalse()) + Expect(obj.Spec.Wandb.ServiceAccount.ServiceAccountName).To(Equal("custom-sa")) + Expect(obj.Spec.MySQL[appsv2.DefaultInstanceName].ManagedMysql.Namespace).To(Equal("custom-moco")) + Expect(obj.Spec.Redis[appsv2.DefaultInstanceName].ManagedRedis.Namespace).To(Equal("custom-redis")) + Expect(obj.Spec.Kafka.ManagedKafka.Namespace).To(Equal("custom-kafka")) + Expect(obj.Spec.ObjectStore[appsv2.DefaultInstanceName].ManagedObjectStore.Namespace).To(Equal("custom-objectstore")) + Expect(obj.Status.Wandb.Applications).To(HaveKey("api")) + }) + + It("returns an error for wrong object type", func() { + err := defaulter.Default(ctx, &corev1.Pod{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("expected an WeightsAndBiases object")) + }) + }) + + Context("When creating or updating WeightsAndBiases under Validating Webhook", func() { + It("allows create when ManagedRedis is nil", func() { + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).To(BeEmpty()) + }) + + It("rejects create when Redis storage size is invalid", func() { + obj.Spec.Redis = map[string]appsv2.RedisSpec{appsv2.DefaultInstanceName: {ManagedRedis: &appsv2.ManagedRedisSpec{StorageSize: "bad-size"}}} + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("storageSize")) + }) + + It("rejects redis namespace changes on update", func() { + oldObj.Spec.Redis = map[string]appsv2.RedisSpec{appsv2.DefaultInstanceName: {ManagedRedis: &appsv2.ManagedRedisSpec{Namespace: "redis-a"}}} + obj.Spec.Redis = map[string]appsv2.RedisSpec{appsv2.DefaultInstanceName: {ManagedRedis: &appsv2.ManagedRedisSpec{Namespace: "redis-b"}}} + + _, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("namespace")) + }) + + It("rejects redis storage size changes when already set", func() { + oldObj.Spec.Redis = map[string]appsv2.RedisSpec{appsv2.DefaultInstanceName: {ManagedRedis: &appsv2.ManagedRedisSpec{Namespace: "redis", StorageSize: "10Gi"}}} + obj.Spec.Redis = map[string]appsv2.RedisSpec{appsv2.DefaultInstanceName: {ManagedRedis: &appsv2.ManagedRedisSpec{Namespace: "redis", StorageSize: "20Gi"}}} + + _, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("storageSize may not be changed")) + }) + + It("allows redis storage size to be initially set on update", func() { + oldObj.Spec.Redis = map[string]appsv2.RedisSpec{appsv2.DefaultInstanceName: {ManagedRedis: &appsv2.ManagedRedisSpec{Namespace: "redis", StorageSize: ""}}} + obj.Spec.Redis = map[string]appsv2.RedisSpec{appsv2.DefaultInstanceName: {ManagedRedis: &appsv2.ManagedRedisSpec{Namespace: "redis", StorageSize: "20Gi"}}} + + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).To(BeEmpty()) + }) + + It("rejects decreasing managed MySQL replicas on update", func() { + oldObj.Spec.MySQL = map[string]appsv2.MySQLSpec{appsv2.DefaultInstanceName: {ManagedMysql: &appsv2.ManagedMysqlSpec{Replicas: 3}}} + obj.Spec.MySQL = map[string]appsv2.MySQLSpec{appsv2.DefaultInstanceName: {ManagedMysql: &appsv2.ManagedMysqlSpec{Replicas: 1}}} + + _, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("replicas cannot be decreased")) + }) + + It("allows increasing managed MySQL replicas on update", func() { + oldObj.Spec.MySQL = map[string]appsv2.MySQLSpec{appsv2.DefaultInstanceName: {ManagedMysql: &appsv2.ManagedMysqlSpec{Replicas: 1}}} + obj.Spec.MySQL = map[string]appsv2.MySQLSpec{appsv2.DefaultInstanceName: {ManagedMysql: &appsv2.ManagedMysqlSpec{Replicas: 3}}} + + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).To(BeEmpty()) + }) + + It("rejects managed ClickHouse when no object store is configured", func() { + obj.Spec.ClickHouse = map[string]appsv2.ClickHouseSpec{appsv2.DefaultInstanceName: {ManagedClickHouse: &appsv2.ManagedClickHouseSpec{}}} + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("object store")) + }) + + It("allows managed ClickHouse when an object store is configured", func() { + obj.Spec.ClickHouse = map[string]appsv2.ClickHouseSpec{appsv2.DefaultInstanceName: {ManagedClickHouse: &appsv2.ManagedClickHouseSpec{}}} + obj.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{appsv2.DefaultInstanceName: {ManagedObjectStore: &appsv2.ManagedObjectStoreSpec{}}} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).To(BeEmpty()) + }) + + It("rejects a managed ClickHouse name whose derived object names cannot be deployed", func() { + obj.Spec.ClickHouse = map[string]appsv2.ClickHouseSpec{appsv2.DefaultInstanceName: {ManagedClickHouse: &appsv2.ManagedClickHouseSpec{Name: "wandb-legacy-overrides-v1-clickhouse"}}} + obj.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{appsv2.DefaultInstanceName: {ManagedObjectStore: &appsv2.ManagedObjectStoreSpec{}}} + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("cannot be deployed")) + Expect(err.Error()).To(ContainSubstring("deploy-confd")) + }) + + It("rejects a managed MySQL name beyond Moco's cluster-name cap", func() { + obj.Spec.MySQL = map[string]appsv2.MySQLSpec{appsv2.DefaultInstanceName: {ManagedMysql: &appsv2.ManagedMysqlSpec{ + Name: "a-managed-mysql-name-that-is-well-past-forty-characters", + }}} + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Moco")) + }) + + It("grandfathers an over-budget name that is unchanged on update", func() { + longName := "wandb-legacy-overrides-v1-clickhouse" + oldObj.Spec.ClickHouse = map[string]appsv2.ClickHouseSpec{appsv2.DefaultInstanceName: {ManagedClickHouse: &appsv2.ManagedClickHouseSpec{Name: longName}}} + oldObj.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{appsv2.DefaultInstanceName: {ManagedObjectStore: &appsv2.ManagedObjectStoreSpec{}}} + obj.Spec.ClickHouse = map[string]appsv2.ClickHouseSpec{appsv2.DefaultInstanceName: {ManagedClickHouse: &appsv2.ManagedClickHouseSpec{Name: longName}}} + obj.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{appsv2.DefaultInstanceName: {ManagedObjectStore: &appsv2.ManagedObjectStoreSpec{}}} + + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).To(BeEmpty()) + }) + + It("rejects changing a name to one that cannot be deployed on update", func() { + oldObj.Spec.ClickHouse = map[string]appsv2.ClickHouseSpec{appsv2.DefaultInstanceName: {ManagedClickHouse: &appsv2.ManagedClickHouseSpec{Name: "wandb-chi"}}} + oldObj.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{appsv2.DefaultInstanceName: {ManagedObjectStore: &appsv2.ManagedObjectStoreSpec{}}} + obj.Spec.ClickHouse = map[string]appsv2.ClickHouseSpec{appsv2.DefaultInstanceName: {ManagedClickHouse: &appsv2.ManagedClickHouseSpec{Name: "wandb-legacy-overrides-v1-clickhouse"}}} + obj.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{appsv2.DefaultInstanceName: {ManagedObjectStore: &appsv2.ManagedObjectStoreSpec{}}} + + _, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("cannot be deployed")) + }) + + It("allows object store copies within the data-node count", func() { + obj.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{appsv2.DefaultInstanceName: {ManagedObjectStore: &appsv2.ManagedObjectStoreSpec{Replicas: 3, Copies: 2}}} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).To(BeEmpty()) + }) + + It("rejects object store copies that exceed replicas-1", func() { + obj.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{appsv2.DefaultInstanceName: {ManagedObjectStore: &appsv2.ManagedObjectStoreSpec{Replicas: 3, Copies: 3}}} + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("copies")) + }) + + It("rejects negative object store copies", func() { + obj.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{appsv2.DefaultInstanceName: {ManagedObjectStore: &appsv2.ManagedObjectStoreSpec{Copies: -1}}} + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("copies")) + }) + + It("allows object store copies when replicas is unset (deferred to reconcile)", func() { + obj.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{appsv2.DefaultInstanceName: {ManagedObjectStore: &appsv2.ManagedObjectStoreSpec{Copies: 2}}} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).To(BeEmpty()) }) + + It("rejects even Keeper replica counts", func() { + obj.Spec.ClickHouse = map[string]appsv2.ClickHouseSpec{appsv2.DefaultInstanceName: {ManagedClickHouse: &appsv2.ManagedClickHouseSpec{ + Keeper: appsv2.ClickHouseKeeperSpec{Replicas: 2}, + }}} + obj.Spec.ObjectStore = map[string]appsv2.ObjectStoreSpec{appsv2.DefaultInstanceName: {ExternalObjectStore: &appsv2.ObjectStoreConnection{}}} + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("odd number")) + }) + + It("rejects gatewayAPI config when mode is ingress", func() { + obj.Spec.Networking.Mode = appsv2.NetworkingModeIngress + obj.Spec.Networking.GatewayAPI = &appsv2.GatewayAPIConfig{ + Gateway: appsv2.GatewayConfig{ + Managed: true, + }, + } + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("gatewayAPI")) + }) + + It("requires gatewayAPI config when mode is gateway", func() { + obj.Spec.Networking.Mode = appsv2.NetworkingModeGatewayAPI + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("gatewayAPI is required")) + }) + + It("requires gatewayClassName for managed gateways", func() { + obj.Spec.Networking.Mode = appsv2.NetworkingModeGatewayAPI + obj.Spec.Networking.GatewayAPI = &appsv2.GatewayAPIConfig{ + Gateway: appsv2.GatewayConfig{ + Managed: true, + }, + } + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("gatewayClassName")) + }) + + It("requires gatewayRef for external gateways", func() { + obj.Spec.Networking.Mode = appsv2.NetworkingModeGatewayAPI + obj.Spec.Networking.GatewayAPI = &appsv2.GatewayAPIConfig{ + Gateway: appsv2.GatewayConfig{ + Managed: false, + }, + } + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("gatewayRef is required")) + }) + + It("returns an error for wrong object type", func() { + _, err := validator.ValidateCreate(ctx, &corev1.Pod{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("expected a WeightsAndBiases object")) + }) + }) + +}) + +func boolPtr(v bool) *bool { + return &v +} diff --git a/pkg/helm/chart.go b/pkg/helm/chart.go index 003c3b6b..68f373bb 100644 --- a/pkg/helm/chart.go +++ b/pkg/helm/chart.go @@ -4,13 +4,14 @@ import ( "fmt" "time" - "helm.sh/helm/v4/pkg/action" - chart "helm.sh/helm/v4/pkg/chart/v2" - chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - "helm.sh/helm/v4/pkg/cli" - "helm.sh/helm/v4/pkg/kube" - "helm.sh/helm/v4/pkg/release" - releasecommon "helm.sh/helm/v4/pkg/release/common" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/releaseutil" + "helm.sh/helm/v3/pkg/repo" ) const ( @@ -18,23 +19,27 @@ const ( maxReleasesToKeep = 10 ) +var ( + noopLogger = func(_ string, _ ...interface{}) {} +) + type ActionableChartInterface interface { Apply( chart *chart.Chart, values map[string]interface{}, - ) (release.Releaser, error) + ) (*release.Release, error) Install( chart *chart.Chart, values map[string]interface{}, - ) (release.Releaser, error) - History() ([]release.Releaser, error) + ) (*release.Release, error) + History() ([]*release.Release, error) Rollback(version int) error - Upgrade(chart *chart.Chart, values map[string]interface{}) (release.Releaser, error) + Upgrade(chart *chart.Chart, values map[string]interface{}) (*release.Release, error) Uninstall() (*release.UninstallReleaseResponse, error) - GetRelease(version int) (release.Releaser, error) + GetRelease(version int) (*release.Release, error) } -// InitConfig returns a helm action configuration. Namespace is used to determine +// GetConfig returns a helm action configuration. Namespace is used to determine // where to store the versions func InitConfig(namespace string) (*cli.EnvSettings, *action.Configuration, error) { settings := cli.New() @@ -44,11 +49,47 @@ func InitConfig(namespace string) (*cli.EnvSettings, *action.Configuration, erro settings.RESTClientGetter(), settings.Namespace(), secretsStorageDriver, + noopLogger, ) config.Releases.MaxHistory = maxReleasesToKeep return settings, config, err } +func DownloadChart(repoURL string, name string) (string, error) { + settings := cli.New() + providers := getter.All(settings) + + entry := new(repo.Entry) + entry.URL = repoURL + entry.Name = name + + file := repo.NewFile() + file.Update(entry) + + chartRepo, err := repo.NewChartRepository(entry, providers) + if err != nil { + return "", err + } + + _, err = chartRepo.DownloadIndexFile() + if err != nil { + return "", err + } + + chartURL, err := repo.FindChartInRepoURL( + repoURL, name, + "", "", "", "", + providers, + ) + if err != nil { + return "", err + } + + client := action.NewPull() + client.Settings = settings + return client.Run(chartURL) +} + func NewActionableChart(releaseName string, namespace string) (*ActionableChart, error) { if err := chartutil.ValidateReleaseName(releaseName); err != nil { return nil, fmt.Errorf("release name %q", releaseName) @@ -78,29 +119,18 @@ func (c *ActionableChart) isInstalled() bool { return false } - // Find the release with the highest revision using the Accessor interface, - // which handles any concrete release type (not just v1). - var latest release.Accessor - for _, r := range h { - acc, err := release.NewAccessor(r) - if err != nil { - continue - } - if latest == nil || acc.Version() > latest.Version() { - latest = acc - } - } - if latest == nil { - return false - } + releaseutil.Reverse(h, releaseutil.SortByRevision) + rel := h[0] + st := rel.Info.Status + + return st != release.StatusUninstalled - return latest.Status() != releasecommon.StatusUninstalled.String() } func (c *ActionableChart) Apply( chart *chart.Chart, values map[string]interface{}, -) (release.Releaser, error) { +) (*release.Release, error) { if c.isInstalled() { return c.Upgrade(chart, values) } @@ -110,42 +140,36 @@ func (c *ActionableChart) Apply( func (c *ActionableChart) Install( chart *chart.Chart, values map[string]interface{}, -) (release.Releaser, error) { +) (*release.Release, error) { client := action.NewInstall(c.config) client.ReleaseName = c.releaseName client.Namespace = c.namespace - client.WaitStrategy = kube.HookOnlyStrategy - client.ServerSideApply = false return client.Run(chart, values) } -func (c *ActionableChart) History() ([]release.Releaser, error) { +func (c *ActionableChart) History() ([]*release.Release, error) { return c.config.Releases.History(c.releaseName) } func (c *ActionableChart) Rollback(version int) error { client := action.NewRollback(c.config) - client.WaitStrategy = kube.HookOnlyStrategy - client.ServerSideApply = "false" return client.Run(c.releaseName) } -func (c *ActionableChart) Upgrade(chart *chart.Chart, values map[string]interface{}) (release.Releaser, error) { +func (c *ActionableChart) Upgrade(chart *chart.Chart, values map[string]interface{}) (*release.Release, error) { client := action.NewUpgrade(c.config) client.Namespace = c.namespace client.MaxHistory = maxReleasesToKeep - client.WaitStrategy = kube.HookOnlyStrategy - client.ServerSideApply = "false" return client.Run(c.releaseName, chart, values) } func (c *ActionableChart) Uninstall() (*release.UninstallReleaseResponse, error) { client := action.NewUninstall(c.config) - client.WaitStrategy = kube.LegacyStrategy + client.Wait = true client.Timeout = 600 * time.Second return client.Run(c.releaseName) } -func (c *ActionableChart) GetRelease(version int) (release.Releaser, error) { +func (c *ActionableChart) GetRelease(version int) (*release.Release, error) { return c.config.Releases.Get(c.releaseName, version) } diff --git a/pkg/utils/coalesce.go b/pkg/utils/coalesce.go new file mode 100644 index 00000000..65457758 --- /dev/null +++ b/pkg/utils/coalesce.go @@ -0,0 +1,29 @@ +package utils + +import ( + "k8s.io/apimachinery/pkg/api/resource" +) + +func Coalesce[T comparable](actual, defaultValue T) T { + var zero T + if actual != zero { + return actual + } + return defaultValue +} + +func CoalesceQuantity(actual, defaultValue string) string { + if actual != "" { + if qty, err := resource.ParseQuantity(actual); err == nil && !qty.IsZero() { + return qty.String() + } + } + + if defaultValue != "" { + if qty, err := resource.ParseQuantity(defaultValue); err == nil && !qty.IsZero() { + return qty.String() + } + } + + return "" +} diff --git a/pkg/utils/coalesce_test.go b/pkg/utils/coalesce_test.go new file mode 100644 index 00000000..38e5c8b6 --- /dev/null +++ b/pkg/utils/coalesce_test.go @@ -0,0 +1,242 @@ +package utils + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Comparable", func() { + Describe("Coalesce", func() { + Context("with string values", func() { + It("should return actual when actual is non-empty", func() { + result := Coalesce("actual", "default") + Expect(result).To(Equal("actual")) + }) + + It("should return default when actual is empty", func() { + result := Coalesce("", "default") + Expect(result).To(Equal("default")) + }) + + It("should return default when both are empty", func() { + result := Coalesce("", "") + Expect(result).To(Equal("")) + }) + + It("should handle empty default with non-empty actual", func() { + result := Coalesce("actual", "") + Expect(result).To(Equal("actual")) + }) + }) + + Context("with integer values", func() { + It("should return actual when actual is non-zero", func() { + result := Coalesce(42, 10) + Expect(result).To(Equal(42)) + }) + + It("should return default when actual is zero", func() { + result := Coalesce(0, 10) + Expect(result).To(Equal(10)) + }) + + It("should handle negative values", func() { + result := Coalesce(-5, 10) + Expect(result).To(Equal(-5)) + }) + + It("should return zero default when actual is zero", func() { + result := Coalesce(0, 0) + Expect(result).To(Equal(0)) + }) + }) + + Context("with boolean values", func() { + It("should return actual when actual is true", func() { + result := Coalesce(true, false) + Expect(result).To(BeTrue()) + }) + + It("should return default when actual is false", func() { + result := Coalesce(false, true) + Expect(result).To(BeTrue()) + }) + + It("should handle both false", func() { + result := Coalesce(false, false) + Expect(result).To(BeFalse()) + }) + }) + + Context("with float values", func() { + It("should return actual when actual is non-zero", func() { + result := Coalesce(3.14, 2.71) + Expect(result).To(Equal(3.14)) + }) + + It("should return default when actual is zero", func() { + result := Coalesce(0.0, 2.71) + Expect(result).To(Equal(2.71)) + }) + + It("should handle negative floats", func() { + result := Coalesce(-1.5, 2.71) + Expect(result).To(Equal(-1.5)) + }) + }) + + Context("with custom comparable types", func() { + type CustomString string + + It("should work with type aliases", func() { + result := Coalesce(CustomString("actual"), CustomString("default")) + Expect(result).To(Equal(CustomString("actual"))) + }) + + It("should return default for empty type alias", func() { + result := Coalesce(CustomString(""), CustomString("default")) + Expect(result).To(Equal(CustomString("default"))) + }) + }) + + Context("integration scenarios", func() { + It("should be usable in merge functions", func() { + actualSize := "10Gi" + defaultSize := "5Gi" + result := Coalesce(actualSize, defaultSize) + Expect(result).To(Equal("10Gi")) + }) + + It("should handle missing actual values", func() { + var actualSize string + defaultSize := "5Gi" + result := Coalesce(actualSize, defaultSize) + Expect(result).To(Equal("5Gi")) + }) + + It("should work with resource quantities", func() { + actualReplicas := int32(5) + defaultReplicas := int32(3) + result := Coalesce(actualReplicas, defaultReplicas) + Expect(result).To(Equal(int32(5))) + }) + + It("should provide fallback for unset replica counts", func() { + actualReplicas := int32(0) + defaultReplicas := int32(3) + result := Coalesce(actualReplicas, defaultReplicas) + Expect(result).To(Equal(int32(3))) + }) + }) + }) + + Describe("CoalesceQuantity", func() { + Context("with valid quantity strings", func() { + It("should return actual when actual is non-empty and valid", func() { + result := CoalesceQuantity("100Mi", "50Mi") + Expect(result).To(Equal("100Mi")) + }) + + It("should return default when actual is empty", func() { + result := CoalesceQuantity("", "50Mi") + Expect(result).To(Equal("50Mi")) + }) + + It("should return empty string when both are empty", func() { + result := CoalesceQuantity("", "") + Expect(result).To(Equal("")) + }) + + It("should handle CPU quantities", func() { + result := CoalesceQuantity("2", "1") + Expect(result).To(Equal("2")) + }) + + It("should handle millicpu quantities", func() { + result := CoalesceQuantity("500m", "250m") + Expect(result).To(Equal("500m")) + }) + + It("should handle various memory units", func() { + result := CoalesceQuantity("2Gi", "1Gi") + Expect(result).To(Equal("2Gi")) + }) + }) + + Context("with zero value", func() { + It("should return default when actual is '0'", func() { + result := CoalesceQuantity("0", "100Mi") + Expect(result).To(Equal("100Mi")) + }) + + It("should return empty string when both are '0'", func() { + result := CoalesceQuantity("0", "0") + Expect(result).To(Equal("")) + }) + + It("should return empty string when actual is '0' and default is empty", func() { + result := CoalesceQuantity("0", "") + Expect(result).To(Equal("")) + }) + + It("should return empty string when actual is empty and default is '0'", func() { + result := CoalesceQuantity("", "0") + Expect(result).To(Equal("")) + }) + + It("should treat '0Mi' as zero and return default", func() { + result := CoalesceQuantity("0Mi", "50Mi") + Expect(result).To(Equal("50Mi")) + }) + + It("should treat '0Gi' as zero and return default", func() { + result := CoalesceQuantity("0Gi", "1Gi") + Expect(result).To(Equal("1Gi")) + }) + }) + + Context("with invalid quantity strings", func() { + It("should return default when actual is invalid", func() { + result := CoalesceQuantity("invalid", "50Mi") + Expect(result).To(Equal("50Mi")) + }) + + It("should return empty string when both are invalid", func() { + result := CoalesceQuantity("invalid", "also-invalid") + Expect(result).To(Equal("")) + }) + + It("should return actual when actual is valid and default is invalid", func() { + result := CoalesceQuantity("100Mi", "invalid") + Expect(result).To(Equal("100Mi")) + }) + }) + + Context("integration scenarios", func() { + It("should handle storage size specifications", func() { + result := CoalesceQuantity("10Gi", "5Gi") + Expect(result).To(Equal("10Gi")) + }) + + It("should provide fallback for missing storage size", func() { + result := CoalesceQuantity("", "5Gi") + Expect(result).To(Equal("5Gi")) + }) + + It("should handle mixed unit formats", func() { + result := CoalesceQuantity("1024Mi", "1Gi") + Expect(result).To(Equal("1Gi")) + }) + + It("should handle very large quantities", func() { + result := CoalesceQuantity("1Ti", "500Gi") + Expect(result).To(Equal("1Ti")) + }) + + It("should handle very small CPU quantities", func() { + result := CoalesceQuantity("100m", "50m") + Expect(result).To(Equal("100m")) + }) + }) + }) +}) diff --git a/pkg/utils/connection_secrets.go b/pkg/utils/connection_secrets.go new file mode 100644 index 00000000..00911cb6 --- /dev/null +++ b/pkg/utils/connection_secrets.go @@ -0,0 +1,38 @@ +package utils + +import ( + "context" + "fmt" + "strings" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// connSecretResolver resolves SecretKeySelectors from an ObjectStoreConnection, +// caching each referenced secret so a connection spanning multiple secrets is +// fetched once per secret. +type ConnSecretResolver struct { + Client client.Client + Namespace string + Cache map[string]*v1.Secret +} + +// value returns the trimmed value the selector points at, or "" if the selector +// is unset or the key is absent. +func (r *ConnSecretResolver) Value(ctx context.Context, sel v1.SecretKeySelector) (string, error) { + if sel.Name == "" || sel.Key == "" { + return "", nil + } + secret, ok := r.Cache[sel.Name] + if !ok { + secret = &v1.Secret{} + key := types.NamespacedName{Namespace: r.Namespace, Name: sel.Name} + if err := r.Client.Get(ctx, key, secret); err != nil { + return "", fmt.Errorf("read object store connection secret %q: %w", key, err) + } + r.Cache[sel.Name] = secret + } + return strings.TrimSpace(string(secret.Data[sel.Key])), nil +} diff --git a/pkg/utils/gvk.go b/pkg/utils/gvk.go new file mode 100644 index 00000000..402c7df6 --- /dev/null +++ b/pkg/utils/gvk.go @@ -0,0 +1,28 @@ +package utils + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" +) + +var serverResources = map[string]bool{} + +func IsRegistered(scheme *runtime.Scheme, obj runtime.Object) bool { + gvks, _, err := scheme.ObjectKinds(obj) + if err != nil || len(gvks) == 0 { + return false + } + return IsServerResource(fmt.Sprintf("%s.%s/%s", gvks[0].Kind, gvks[0].Group, gvks[0].Version)) +} + +func AddServerResource(resource string) { + serverResources[resource] = true +} + +func IsServerResource(resource string) bool { + if value, exists := serverResources[resource]; exists && value { + return true + } + return false +} diff --git a/pkg/utils/gvk_test.go b/pkg/utils/gvk_test.go new file mode 100644 index 00000000..8f5fcb7c --- /dev/null +++ b/pkg/utils/gvk_test.go @@ -0,0 +1,32 @@ +package utils + +import ( + "testing" + + "k8s.io/apimachinery/pkg/runtime" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func TestIsRegisteredUsesKindGroupVersion(t *testing.T) { + scheme := runtime.NewScheme() + if err := gatewayv1.Install(scheme); err != nil { + t.Fatalf("install gateway api scheme: %v", err) + } + + t.Run("returns true when server resource key uses version", func(t *testing.T) { + serverResources = map[string]bool{} + AddServerResource("HTTPRoute.gateway.networking.k8s.io/v1") + + if !IsRegistered(scheme, &gatewayv1.HTTPRoute{}) { + t.Fatalf("expected HTTPRoute to be registered") + } + }) + + t.Run("returns false when server resource key is missing", func(t *testing.T) { + serverResources = map[string]bool{} + + if IsRegistered(scheme, &gatewayv1.HTTPRoute{}) { + t.Fatalf("expected HTTPRoute to be unregistered") + } + }) +} diff --git a/pkg/utils/maps.go b/pkg/utils/maps.go new file mode 100644 index 00000000..9b65728a --- /dev/null +++ b/pkg/utils/maps.go @@ -0,0 +1,15 @@ +package utils + +func MapMerge[K comparable, V any](actual, defaultValues map[K]V) map[K]V { + result := make(map[K]V) + + for k, v := range defaultValues { + result[k] = v + } + + for k, v := range actual { + result[k] = v + } + + return result +} diff --git a/pkg/utils/maps_test.go b/pkg/utils/maps_test.go new file mode 100644 index 00000000..3921c020 --- /dev/null +++ b/pkg/utils/maps_test.go @@ -0,0 +1,161 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMergeMap(t *testing.T) { + t.Run("merges maps with actual overriding defaults", func(t *testing.T) { + defaults := map[string]string{ + "key1": "default1", + "key2": "default2", + "key3": "default3", + } + actual := map[string]string{ + "key2": "actual2", + "key4": "actual4", + } + + result := MapMerge(actual, defaults) + + expected := map[string]string{ + "key1": "default1", + "key2": "actual2", + "key3": "default3", + "key4": "actual4", + } + assert.Equal(t, expected, result) + }) + + t.Run("returns defaults when actual is empty", func(t *testing.T) { + defaults := map[string]int{ + "a": 1, + "b": 2, + } + actual := map[string]int{} + + result := MapMerge(actual, defaults) + + assert.Equal(t, defaults, result) + }) + + t.Run("returns actual when defaults is empty", func(t *testing.T) { + defaults := map[string]int{} + actual := map[string]int{ + "x": 10, + "y": 20, + } + + result := MapMerge(actual, defaults) + + assert.Equal(t, actual, result) + }) + + t.Run("returns empty map when both are empty", func(t *testing.T) { + defaults := map[string]string{} + actual := map[string]string{} + + result := MapMerge(actual, defaults) + + assert.Empty(t, result) + }) + + t.Run("handles nil maps", func(t *testing.T) { + var defaults map[string]string + var actual map[string]string + + result := MapMerge(actual, defaults) + + assert.Empty(t, result) + }) + + t.Run("handles nil actual with non-nil defaults", func(t *testing.T) { + defaults := map[string]string{ + "key1": "value1", + } + var actual map[string]string + + result := MapMerge(actual, defaults) + + assert.Equal(t, defaults, result) + }) + + t.Run("handles nil defaults with non-nil actual", func(t *testing.T) { + var defaults map[string]string + actual := map[string]string{ + "key1": "value1", + } + + result := MapMerge(actual, defaults) + + assert.Equal(t, actual, result) + }) + + t.Run("works with integer keys", func(t *testing.T) { + defaults := map[int]string{ + 1: "one", + 2: "two", + } + actual := map[int]string{ + 2: "TWO", + 3: "three", + } + + result := MapMerge(actual, defaults) + + expected := map[int]string{ + 1: "one", + 2: "TWO", + 3: "three", + } + assert.Equal(t, expected, result) + }) + + t.Run("works with struct values", func(t *testing.T) { + type Value struct { + Name string + Age int + } + + defaults := map[string]Value{ + "alice": {Name: "Alice", Age: 30}, + "bob": {Name: "Bob", Age: 25}, + } + actual := map[string]Value{ + "bob": {Name: "Bob", Age: 26}, + "charlie": {Name: "Charlie", Age: 35}, + } + + result := MapMerge(actual, defaults) + + expected := map[string]Value{ + "alice": {Name: "Alice", Age: 30}, + "bob": {Name: "Bob", Age: 26}, + "charlie": {Name: "Charlie", Age: 35}, + } + assert.Equal(t, expected, result) + }) + + t.Run("does not modify input maps", func(t *testing.T) { + defaults := map[string]string{ + "key1": "default1", + } + actual := map[string]string{ + "key2": "actual2", + } + + defaultsCopy := map[string]string{ + "key1": "default1", + } + actualCopy := map[string]string{ + "key2": "actual2", + } + + _ = MapMerge(actual, defaults) + + assert.Equal(t, defaultsCopy, defaults) + assert.Equal(t, actualCopy, actual) + }) +} diff --git a/pkg/utils/merge.go b/pkg/utils/merge.go index c95a680c..fd14a32d 100644 --- a/pkg/utils/merge.go +++ b/pkg/utils/merge.go @@ -84,3 +84,22 @@ func MergeMapString(a, b map[string]interface{}) (map[string]interface{}, error) return nil, fmt.Errorf("failed to merge map[string]interface{}: %v", mr) } + +func MergeMapsStringString(maps ...map[string]string) map[string]string { + dst := map[string]string{} + for _, m := range maps { + dst = MergeMapStringString(dst, m) + } + return dst +} + +func MergeMapStringString(a, b map[string]string) map[string]string { + dst := map[string]string{} + for k, v := range a { + dst[k] = v + } + for k, v := range b { + dst[k] = v + } + return dst +} diff --git a/pkg/utils/openshift.go b/pkg/utils/openshift.go new file mode 100644 index 00000000..33181232 --- /dev/null +++ b/pkg/utils/openshift.go @@ -0,0 +1,25 @@ +package utils + +import ( + "os" + "strconv" + "sync/atomic" +) + +var openshiftMode atomic.Bool + +func init() { + if enabled, err := strconv.ParseBool(os.Getenv("OPENSHIFT")); err == nil && enabled { + openshiftMode.Store(true) + } +} + +// SetOpenShiftMode enables or disables OpenShift rendering behavior. +func SetOpenShiftMode(enabled bool) { + openshiftMode.Store(enabled) +} + +// IsOpenShift reports whether OpenShift-specific rendering behavior is enabled. +func IsOpenShift() bool { + return openshiftMode.Load() +} diff --git a/pkg/utils/slices.go b/pkg/utils/slices.go new file mode 100644 index 00000000..d9ce0e51 --- /dev/null +++ b/pkg/utils/slices.go @@ -0,0 +1,40 @@ +package utils + +func FilterFunc[T any](s []T, predicate func(T) bool) []T { + var result []T + for _, v := range s { + if predicate(v) { + result = append(result, v) + } + } + return result +} + +func ContainsFunc[T any](s []T, predicate func(T) bool) bool { + for _, v := range s { + if predicate(v) { + return true + } + } + return false +} + +func MapFunc[T any, U any](s []T, f func(T) U) []U { + var result []U + for _, v := range s { + result = append(result, f(v)) + } + return result +} + +func FilterMapFunc[T any, U any](s []T, f func(T) (U, bool)) []U { + var result []U + for _, v := range s { + next, ok := f(v) + if ok { + result = append(result, next) + } + + } + return result +} diff --git a/pkg/utils/strings.go b/pkg/utils/strings.go new file mode 100644 index 00000000..d2381bd8 --- /dev/null +++ b/pkg/utils/strings.go @@ -0,0 +1,53 @@ +package utils + +import ( + "crypto/rand" + "math/big" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +func ContainsString(strings []string, target string) bool { + for _, s := range strings { + if s == target { + return true + } + } + return false +} + +func RemoveString(strings []string, target string) []string { + result := []string{} + for _, s := range strings { + if s != target { + result = append(result, s) + } + } + return result +} + +func GenerateRandomPassword(length int) (string, error) { + allowedCharacters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#%^*()-_=+{}|,.<>?" + characterSetLength := len(allowedCharacters) + + result := make([]byte, length) + for i := 0; i < length; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(int64(characterSetLength))) + if err != nil { + return "", err + } + result[i] = allowedCharacters[num.Int64()] + } + return string(result), nil +} + +func Capitalize(value string) string { + if len(value) == 0 { + return "" + } + capitalizer := cases.Title(language.English) + first := ([]rune)(value)[:1] + tail := ([]rune)(value)[1:] + return capitalizer.String(string(first)) + string(tail) +} diff --git a/pkg/utils/utils_suite_test.go b/pkg/utils/utils_suite_test.go new file mode 100644 index 00000000..847bc965 --- /dev/null +++ b/pkg/utils/utils_suite_test.go @@ -0,0 +1,13 @@ +package utils + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestUtils(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Utils Suite") +} diff --git a/pkg/vendored/altinity-clickhouse/README.md b/pkg/vendored/altinity-clickhouse/README.md new file mode 100644 index 00000000..9ff50764 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/README.md @@ -0,0 +1,154 @@ +# Altinity ClickHouse Operator API Vendored Code + +This directory contains vendored API types from the [Altinity ClickHouse Operator](https://github.com/Altinity/clickhouse-operator) project. + +## Source + +- **Repository**: https://github.com/Altinity/clickhouse-operator +- **Version**: release-0.26.3 +- **Date Vendored**: 2026-04-23 + +## Reason for Vendoring + +To have full control over the CRD API types and avoid unexpected breaking changes when the upstream operator updates. This allows the W&B operator to manage ClickHouse installations while controlling when and how we adopt upstream changes. + +Additionally, the upstream operator has several code generation issues that prevent direct use: +- DeepCopy methods contain mutex copying errors that fail go vet +- Many interface types that are incompatible with controller-gen +- Complex dependencies on internal packages + +## Changes Made + +### No Code Generation or Annotations + +**Important**: Unlike our own API types (api/v1, api/v2), we do NOT run controller-gen on ANY vendored code. The vendored types and their DeepCopy methods are used as-is from upstream. This means: + +- No code generation annotations needed (`+kubebuilder:*`, `+k8s:*`, `+genclient`) +- DeepCopy methods already exist in `zz_generated.deepcopy.go` from upstream +- We only fix compilation errors, not regenerate code + +All code generation annotations have been removed: +- `+k8s:*` annotations (e.g., `+k8s:deepcopy-gen:*`, `+k8s:openapi-gen=true`) + +Files affected: +- `clickhouse.altinity.com/v1/doc.go` +- `clickhouse.altinity.com/v1/types.go` +- `clickhouse.altinity.com/v1/api_resources.go` + +The Makefile's `generate` target only processes `./api/v1` and `./api/v2`, explicitly excluding all vendored APIs: +- `pkg/vendored/altinity-clickhouse/...` +- `pkg/vendored/percona-operator/...` +- `pkg/vendored/minio-operator/...` +- `pkg/vendored/redis-operator/...` +- `pkg/vendored/seaweedfs-operator/...` + +### DeepCopy Mutex Fixes + +The following DeepCopyInto methods in `clickhouse.altinity.com/v1/zz_generated.deepcopy.go` were fixed to avoid copying sync.Mutex and sync.RWMutex: + +- **ClickHouseInstallation** (lines 571-589): Removed `*out = *in` shallow copy, commented out mutex field copies +- **ClickHouseInstallationRuntime** (lines 643-660): Removed `*out = *in` shallow copy, commented out `commonConfigMutex` copy +- **ClickHouseInstallationTemplate** (lines 673-691): Removed `*out = *in` shallow copy, commented out mutex field copies +- **ClickHouseOperatorConfiguration** (lines 745-751): Removed `*out = *in` shallow copy +- **OperatorConfig** (lines 1456-1502): Removed `*out = *in` shallow copy +- **OperatorConfigCHI** (lines 1585-1589): Removed `*out = *in` shallow copy +- **OperatorConfigCHIRuntime** (lines 1602-1624): Removed `*out = *in` shallow copy, commented out `mutex` field copy +- **OperatorConfigTemplate** (lines 2034-2038): Removed `*out = *in` shallow copy +- **Status** (lines 2656-2747): Removed `*out = *in` shallow copy, explicitly copied all non-mutex fields, commented out `mu` field copy + +The same class of fixes was applied to +`clickhouse-keeper.altinity.com/v1/zz_generated.deepcopy.go` (commented out the +`*out = *in` shallow copy and the mutex field copies, explicitly copying the +non-mutex scalar fields for `Status`): + +- **ClickHouseKeeperInstallation** — commented out the shallow copy and the + `statusCreatorMutex` / `runtimeCreatorMutex` copies +- **ClickHouseKeeperInstallationRuntime** — commented out the shallow copy and + the `commonConfigMutex` copy +- **Status** — replaced the shallow copy with explicit non-mutex field copies and + commented out the `mu` (sync.RWMutex) copy + +### Mutex Copy in MergeFrom + +- **clickhouse.altinity.com/v1/type_configuration_chop.go** (line 816): Changed `mergo.Merge(c, *from, ...)` to `mergo.Merge(c, from, ...)` to pass pointer instead of dereferencing (which would copy mutexes) + +### Struct Tag Fixes + +- **clickhouse.altinity.com/v1/type_configuration_chop.go** (lines 194-195): Fixed missing closing quotes in struct tags for `OperatorConfigWatchNamespaces` fields + +### Generic Type Format String Fix + +- **util/map.go** (line 248): Format specifier is `%v` to support any comparable type in generic map printing function (already present in upstream 0.26.3) + +### Unexported Field JSON Tag Fixes + +- **clickhouse.altinity.com/v1/type_template_indexes.go**: Removed json/yaml tags from unexported `templates` fields in: + - `HostTemplatesIndex` (line 20) + - `PodTemplatesIndex` (line 74) + - `VolumeClaimTemplatesIndex` (line 128) + - `ServiceTemplatesIndex` (line 182) + +### Import Path Updates + +All import paths have been updated from `github.com/altinity/clickhouse-operator/pkg/` to the vendored paths: +- `github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1` +- `github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/` +- `github.com/wandb/operator/pkg/vendored/altinity-clickhouse/deployment/` +- `github.com/wandb/operator/pkg/vendored/altinity-clickhouse/metrics/` +- `github.com/wandb/operator/pkg/vendored/altinity-clickhouse/swversion/` +- `github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util/` +- `github.com/wandb/operator/pkg/vendored/altinity-clickhouse/version/` +- `github.com/wandb/operator/pkg/vendored/altinity-clickhouse/xml/` + +## What Was Vendored + +We vendored the API type definitions and utility packages needed for our operator: + +### CRDs + +CRD files are located in `pkg/vendored/altinity-clickhouse/crds/`: +- `clickhouse-operator-install-bundle.yaml` +- `clickhouse.altinity.com_clickhouseinstallations.yaml` (individual CRD) + +**Purpose**: Integration testing with real Kubernetes API server (envtest) + +### API Types +- `clickhouse.altinity.com/v1/` - Main ClickHouseInstallation CRD types + - All type definition files (`type_*.go`) + - Generated DeepCopy methods (`zz_generated.deepcopy.go`) + - API registration and scheme builder + - Configuration helpers +- `clickhouse-keeper.altinity.com/v1/` - ClickHouseKeeperInstallation (CHK) CRD + types, used to provision the ClickHouse Keeper ensemble that backs + ReplicatedMergeTree replication. Same upstream version (release-0.26.3); reuses + many shared types from `clickhouse.altinity.com/v1`. + - `api_group.go` parent package holding `APIGroupName` + (`clickhouse-keeper.altinity.com`) + - Same import-path rewrites as the CHI package (see "Import Path Updates") + - Same DeepCopy mutex fixes applied (see below) + +### Supporting Packages +- `common/` - Common types and constants shared across the operator +- `deployment/` - Deployment-related types and utilities +- `metrics/` - Metrics collection types and helpers +- `swversion/` - Software version handling and parsing +- `util/` - Complete utility package with helper functions +- `version/` - Version information and utilities +- `xml/` - XML configuration handling + +### Removed Content +- All test files (`*_test.go`) +- Benchmark files (`*_bench_test.go`) +- Test data directories + +## Known Issues + +None. All go vet errors and warnings from the vendored code have been fixed. + +## License + +The vendored code maintains its original Apache 2.0 license from the Altinity ClickHouse Operator project. + +## Removal + +This vendored copy should be reviewed when upgrading to newer versions of the ClickHouse operator. Note that any new version will likely require similar mutex copying fixes in the DeepCopy methods, as this is a pattern throughout the upstream codebase. diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/api_group.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/api_group.go new file mode 100644 index 00000000..3731453c --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/api_group.go @@ -0,0 +1,20 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package clickhouse_keeper_altinity_com + +const ( + // APIGroupName is the group name of the ClickHouse Keeper Operator API. + APIGroupName = "clickhouse-keeper.altinity.com" +) diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/api_register.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/api_register.go new file mode 100644 index 00000000..46157c68 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/api_register.go @@ -0,0 +1,45 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" + + clickhouse_keeper_altinity_com "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com" +) + +var ( + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{ + Group: clickhouse_keeper_altinity_com.APIGroupName, + Version: APIVersion, + } + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{ + GroupVersion: SchemeGroupVersion, + } + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +func init() { + SchemeBuilder.Register( + &ClickHouseKeeperInstallation{}, + &ClickHouseKeeperInstallationList{}, + ) +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/api_resources.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/api_resources.go new file mode 100644 index 00000000..d13fa3c8 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/api_resources.go @@ -0,0 +1,24 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +k8s:deepcopy-gen=package,register +// +groupName=clickhouse.altinity.com + +// Package v1 defines version 1 of the API used with ClickHouse Installation Custom Resources. +package v1 + +// Possible kinds of CRDs +const ( + ClickHouseKeeperInstallationCRDResourceKind = "ClickHouseKeeperInstallation" +) diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/api_version.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/api_version.go new file mode 100644 index 00000000..c3c15ba2 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/api_version.go @@ -0,0 +1,20 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +const ( + // APIVersion is the version of the ClickHouse Keeper Operator API. + APIVersion = "v1" +) diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/doc.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/doc.go new file mode 100644 index 00000000..68e204c7 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/doc.go @@ -0,0 +1,19 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +k8s:deepcopy-gen=package +// +groupName=clickhouse-keeper.altinity.com + +// Package v1 defines version 1 of the API used with ClickHouseKeeper custom resource. +package v1 diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_chk.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_chk.go new file mode 100644 index 00000000..e5b5d892 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_chk.go @@ -0,0 +1,715 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/swversion" + + "github.com/imdario/mergo" + "gopkg.in/yaml.v3" + + apiChi "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" +) + +func (cr *ClickHouseKeeperInstallation) GetSpec() apiChi.ICRSpec { + return &cr.Spec +} + +func (cr *ClickHouseKeeperInstallation) GetSpecT() *ChkSpec { + return &cr.Spec +} + +func (cr *ClickHouseKeeperInstallation) GetSpecA() any { + return &cr.Spec +} + +func (cr *ClickHouseKeeperInstallation) GetRuntime() apiChi.ICustomResourceRuntime { + return cr.EnsureRuntime() +} + +func (cr *ClickHouseKeeperInstallation) EnsureRuntime() *ClickHouseKeeperInstallationRuntime { + if cr == nil { + return nil + } + + // Assume that most of the time, we'll see a non-nil value. + if cr.runtime != nil { + return cr.runtime + } + + // Otherwise, we need to acquire a lock to initialize the field. + cr.runtimeCreatorMutex.Lock() + defer cr.runtimeCreatorMutex.Unlock() + // Note that we have to check this property again to avoid a TOCTOU bug. + if cr.runtime == nil { + cr.runtime = newClickHouseKeeperInstallationRuntime() + } + return cr.runtime +} + +func (cr *ClickHouseKeeperInstallation) IEnsureStatus() apiChi.IStatus { + return any(cr.EnsureStatus()).(apiChi.IStatus) +} + +// EnsureStatus ensures status +func (cr *ClickHouseKeeperInstallation) EnsureStatus() *Status { + if cr == nil { + return nil + } + + // Assume that most of the time, we'll see a non-nil value. + if cr.Status != nil { + return cr.Status + } + + // Otherwise, we need to acquire a lock to initialize the field. + cr.statusCreatorMutex.Lock() + defer cr.statusCreatorMutex.Unlock() + // Note that we have to check this property again to avoid a TOCTOU bug. + if cr.Status == nil { + cr.Status = &Status{} + } + return cr.Status +} + +// GetStatus gets Status +func (cr *ClickHouseKeeperInstallation) GetStatus() apiChi.IStatus { + if cr == nil { + return (*Status)(nil) + } + return cr.Status +} + +// HasStatus checks whether CHI has Status +func (cr *ClickHouseKeeperInstallation) HasStatus() bool { + if cr == nil { + return false + } + return cr.Status != nil +} + +// HasAncestor checks whether CR has an ancestor +func (cr *ClickHouseKeeperInstallation) HasAncestor() bool { + if !cr.HasStatus() { + return false + } + return cr.Status.HasNormalizedCRCompleted() +} + +// GetAncestor gets ancestor of a CR +func (cr *ClickHouseKeeperInstallation) GetAncestor() apiChi.ICustomResource { + if !cr.HasAncestor() { + return (*ClickHouseKeeperInstallation)(nil) + } + return cr.Status.GetNormalizedCRCompleted() +} + +// GetAncestorT gets ancestor of a CR +func (cr *ClickHouseKeeperInstallation) GetAncestorT() *ClickHouseKeeperInstallation { + if !cr.HasAncestor() { + return nil + } + return cr.Status.GetNormalizedCRCompleted() +} + +// SetAncestor sets ancestor of a CR +func (cr *ClickHouseKeeperInstallation) SetAncestor(a *ClickHouseKeeperInstallation) { + if cr == nil { + return + } + cr.EnsureStatus().NormalizedCRCompleted = a +} + +// HasTarget checks whether CR has a target +func (cr *ClickHouseKeeperInstallation) HasTarget() bool { + if !cr.HasStatus() { + return false + } + return cr.Status.HasNormalizedCR() +} + +// GetTarget gets target of a CR +func (cr *ClickHouseKeeperInstallation) GetTarget() *ClickHouseKeeperInstallation { + if !cr.HasTarget() { + return nil + } + return cr.Status.GetNormalizedCR() +} + +// SetTarget sets target of a CR +func (cr *ClickHouseKeeperInstallation) SetTarget(a *ClickHouseKeeperInstallation) { + if cr == nil { + return + } + cr.EnsureStatus().NormalizedCR = a +} + +func (cr *ClickHouseKeeperInstallation) GetUsedTemplates() []*apiChi.TemplateRef { + return nil +} + +// FillStatus fills .Status +func (cr *ClickHouseKeeperInstallation) FillStatus(endpoints util.Slice[string], pods, fqdns []string, ip string) { + cr.EnsureStatus().Fill(&FillStatusParams{ + CHOpIP: ip, + ClustersCount: cr.ClustersCount(), + ShardsCount: cr.ShardsCount(), + HostsCount: cr.HostsCount(), + TaskID: cr.GetSpecT().GetTaskID().Value(), + HostsUpdatedCount: 0, + HostsAddedCount: 0, + HostsUnchangedCount: 0, + HostsCompletedCount: 0, + HostsDeleteCount: 0, + HostsDeletedCount: 0, + Pods: pods, + FQDNs: fqdns, + Endpoint: endpoints.First(), + Endpoints: append([]string{}, endpoints...), + NormalizedCR: cr.Copy(types.CopyCROptions{ + SkipStatus: true, + SkipManagedFields: true, + }), + }) +} + +func (cr *ClickHouseKeeperInstallation) Fill() { + apiChi.FillCR(cr) +} + +// MergeFrom merges from CHI +func (cr *ClickHouseKeeperInstallation) MergeFrom(from *ClickHouseKeeperInstallation, _type apiChi.MergeType) { + if from == nil { + return + } + + // Merge Meta + switch _type { + case apiChi.MergeTypeFillEmptyValues: + _ = mergo.Merge(&cr.TypeMeta, from.TypeMeta) + _ = mergo.Merge(&cr.ObjectMeta, from.ObjectMeta) + case apiChi.MergeTypeOverrideByNonEmptyValues: + _ = mergo.Merge(&cr.TypeMeta, from.TypeMeta, mergo.WithOverride) + _ = mergo.Merge(&cr.ObjectMeta, from.ObjectMeta, mergo.WithOverride) + } + // Exclude skipped annotations + cr.SetAnnotations( + util.CopyMapFilter( + cr.GetAnnotations(), + nil, + util.ListSkippedAnnotations(), + ), + ) + + // Do actual merge for Spec + cr.GetSpecT().MergeFrom(from.GetSpecT(), _type) + + // Copy service attributes + //cr.ensureRuntime().attributes = from.ensureRuntime().attributes + + cr.EnsureStatus().CopyFrom(from.Status, types.CopyStatusOptions{ + CopyStatusFieldGroup: types.CopyStatusFieldGroup{ + FieldGroupInheritable: true, + }, + }) +} + +// FindCluster finds cluster by name or index. +// Expectations: name is expected to be a string, index is expected to be an int. +func (cr *ClickHouseKeeperInstallation) FindCluster(needle interface{}) apiChi.ICluster { + var resultCluster *Cluster + cr.WalkClustersFullPath(func(chk *ClickHouseKeeperInstallation, clusterIndex int, cluster *Cluster) error { + switch v := needle.(type) { + case string: + if cluster.Name == v { + resultCluster = cluster + } + case int: + if clusterIndex == v { + resultCluster = cluster + } + } + return nil + }) + return resultCluster +} + +// FindShard finds shard by name or index +// Expectations: name is expected to be a string, index is expected to be an int. +func (cr *ClickHouseKeeperInstallation) FindShard(needleCluster interface{}, needleShard interface{}) apiChi.IShard { + return cr.FindCluster(needleCluster).FindShard(needleShard) +} + +// FindHost finds shard by name or index +// Expectations: name is expected to be a string, index is expected to be an int. +func (cr *ClickHouseKeeperInstallation) FindHost(needleCluster interface{}, needleShard interface{}, needleHost interface{}) *apiChi.Host { + return cr.FindCluster(needleCluster).FindHost(needleShard, needleHost) +} + +// ClustersCount counts clusters +func (cr *ClickHouseKeeperInstallation) ClustersCount() int { + count := 0 + cr.WalkClusters(func(cluster apiChi.ICluster) error { + count++ + return nil + }) + return count +} + +// ShardsCount counts shards +func (cr *ClickHouseKeeperInstallation) ShardsCount() int { + count := 0 + cr.WalkShards(func(shard *ChkShard) error { + count++ + return nil + }) + return count +} + +// HostsCount counts hosts +func (cr *ClickHouseKeeperInstallation) HostsCount() int { + count := 0 + cr.WalkHosts(func(host *apiChi.Host) error { + count++ + return nil + }) + return count +} + +// HostsWithAttributesCount counts hosts by attributes +func (cr *ClickHouseKeeperInstallation) HostsWithAttributesCount(a *types.ReconcileAttributes) int { + count := 0 + cr.WalkHosts(func(host *apiChi.Host) error { + if host.GetReconcileAttributes().HasIntersectionWith(a) { + count++ + } + return nil + }) + return count +} + +// HasReconcileWork reports whether the CR has any work to reconcile: +// either the ActionPlan has spec/label/finalizer changes, or child resources have drifted. +func (cr *ClickHouseKeeperInstallation) HasReconcileWork() bool { + return cr.EnsureRuntime().ActionPlan.HasActionsToDo() || cr.GetHostsAttributesCounters().HasDrift() +} + +// GetHostsAttributesCounters +func (cr *ClickHouseKeeperInstallation) GetHostsAttributesCounters() *types.ReconcileAttributesCounters { + counters := types.NewReconcileAttributesCounters() + cr.WalkHosts(func(host *apiChi.Host) error { + counters.Add(host.GetReconcileAttributes()) + return nil + }) + return counters +} + +// GetHostTemplate gets HostTemplate by name +func (cr *ClickHouseKeeperInstallation) GetHostTemplate(name string) (*apiChi.HostTemplate, bool) { + if !cr.GetSpecT().GetTemplates().GetHostTemplatesIndex().Has(name) { + return nil, false + } + return cr.GetSpecT().GetTemplates().GetHostTemplatesIndex().Get(name), true +} + +// GetPodTemplate gets PodTemplate by name +func (cr *ClickHouseKeeperInstallation) GetPodTemplate(name string) (*apiChi.PodTemplate, bool) { + if !cr.GetSpecT().GetTemplates().GetPodTemplatesIndex().Has(name) { + return nil, false + } + return cr.GetSpecT().GetTemplates().GetPodTemplatesIndex().Get(name), true +} + +// WalkPodTemplates walks over all PodTemplates +func (cr *ClickHouseKeeperInstallation) WalkPodTemplates(f func(template *apiChi.PodTemplate)) { + cr.GetSpecT().GetTemplates().GetPodTemplatesIndex().Walk(f) +} + +// GetVolumeClaimTemplate gets VolumeClaimTemplate by name +func (cr *ClickHouseKeeperInstallation) GetVolumeClaimTemplate(name string) (*apiChi.VolumeClaimTemplate, bool) { + if cr.GetSpecT().GetTemplates().GetVolumeClaimTemplatesIndex().Has(name) { + return cr.GetSpecT().GetTemplates().GetVolumeClaimTemplatesIndex().Get(name), true + } + return nil, false +} + +// WalkVolumeClaimTemplates walks over all VolumeClaimTemplates +func (cr *ClickHouseKeeperInstallation) WalkVolumeClaimTemplates(f func(template *apiChi.VolumeClaimTemplate)) { + if cr == nil { + return + } + cr.GetSpecT().GetTemplates().GetVolumeClaimTemplatesIndex().Walk(f) +} + +// GetServiceTemplate gets ServiceTemplate by name +func (cr *ClickHouseKeeperInstallation) GetServiceTemplate(name string) (*apiChi.ServiceTemplate, bool) { + if !cr.GetSpecT().GetTemplates().GetServiceTemplatesIndex().Has(name) { + return nil, false + } + return cr.GetSpecT().GetTemplates().GetServiceTemplatesIndex().Get(name), true +} + +// GetServiceTemplates gets ServiceTemplates by name +func (cr *ClickHouseKeeperInstallation) GetServiceTemplates(names ...string) ([]*apiChi.ServiceTemplate, bool) { + if len(names) == 0 { + return nil, false + } + var res []*apiChi.ServiceTemplate + for _, name := range names { + if cr.GetSpecT().GetTemplates().GetServiceTemplatesIndex().Has(name) { + res = append(res, cr.GetSpecT().GetTemplates().GetServiceTemplatesIndex().Get(name)) + } + } + if len(res) == len(names) { + return res, true + } + return nil, false +} + +// GetRootServiceTemplates gets service templates of a CR +func (cr *ClickHouseKeeperInstallation) GetRootServiceTemplates() ([]*apiChi.ServiceTemplate, bool) { + if !cr.GetSpecT().GetDefaults().Templates.HasAnyServiceTemplate() { + return nil, false + } + return cr.GetServiceTemplates(cr.GetSpecT().GetDefaults().Templates.GetAllServiceTemplates()...) +} + +// MatchNamespace matches namespace +func (cr *ClickHouseKeeperInstallation) MatchNamespace(namespace string) bool { + if cr == nil { + return false + } + return cr.Namespace == namespace +} + +// MatchFullName matches full name +func (cr *ClickHouseKeeperInstallation) MatchFullName(namespace, name string) bool { + if cr == nil { + return false + } + return (cr.Namespace == namespace) && (cr.Name == name) +} + +// FoundIn checks whether CHI can be found in haystack +func (cr *ClickHouseKeeperInstallation) FoundIn(haystack []*ClickHouseKeeperInstallation) bool { + if cr == nil { + return false + } + + for _, candidate := range haystack { + if candidate.MatchFullName(cr.Namespace, cr.Name) { + return true + } + } + + return false +} + +// IsAuto checks whether templating policy is auto +func (cr *ClickHouseKeeperInstallation) IsAuto() bool { + return false +} + +// IsStopped checks whether CR is stopped +func (cr *ClickHouseKeeperInstallation) IsStopped() bool { + if cr == nil { + return false + } + return cr.GetSpecT().GetStop().Value() +} + +// IsRollingUpdate checks whether CHI should perform rolling update +func (cr *ClickHouseKeeperInstallation) IsRollingUpdate() bool { + return false +} + +// IsTroubleshoot checks whether CHI is in troubleshoot mode +func (cr *ClickHouseKeeperInstallation) IsTroubleshoot() bool { + return false +} + +// GetReconcile gets reconcile spec +func (cr *ClickHouseKeeperInstallation) GetReconcile() *apiChi.ChiReconcile { + if cr == nil { + return nil + } + return cr.GetSpecT().Reconcile +} + +// Copy makes copy of a CHI, filtering fields according to specified CopyOptions +func (cr *ClickHouseKeeperInstallation) Copy(opts types.CopyCROptions) *ClickHouseKeeperInstallation { + if cr == nil { + return nil + } + jsonBytes, err := json.Marshal(cr) + if err != nil { + return nil + } + + var cr2 *ClickHouseKeeperInstallation + if err := json.Unmarshal(jsonBytes, &cr2); err != nil { + return nil + } + + if opts.SkipStatus { + cr2.Status = nil + } + + if opts.SkipManagedFields { + cr2.SetManagedFields(nil) + } + + return cr2 +} + +// JSON returns JSON string +func (cr *ClickHouseKeeperInstallation) JSON(opts types.CopyCROptions) string { + if cr == nil { + return "" + } + + filtered := cr.Copy(opts) + jsonBytes, err := json.MarshalIndent(filtered, "", " ") + if err != nil { + return fmt.Sprintf("unable to parse. err: %v", err) + } + return string(jsonBytes) + +} + +// YAML return YAML string +func (cr *ClickHouseKeeperInstallation) YAML(opts types.CopyCROptions) string { + if cr == nil { + return "" + } + + filtered := cr.Copy(opts) + yamlBytes, err := yaml.Marshal(filtered) + if err != nil { + return fmt.Sprintf("unable to parse. err: %v", err) + } + return string(yamlBytes) +} + +// FirstHost returns first host of the CR +func (cr *ClickHouseKeeperInstallation) FirstHost() *apiChi.Host { + var result *apiChi.Host + cr.WalkHosts(func(host *apiChi.Host) error { + if result == nil { + result = host + } + return nil + }) + return result +} + +func (cr *ClickHouseKeeperInstallation) GetName() string { + if cr == nil { + return "" + } + return cr.Name +} + +func (cr *ClickHouseKeeperInstallation) GetNamespace() string { + if cr == nil { + return "" + } + return cr.Namespace +} + +func (cr *ClickHouseKeeperInstallation) GetLabels() map[string]string { + if cr == nil { + return nil + } + return cr.Labels +} + +func (cr *ClickHouseKeeperInstallation) GetAnnotations() map[string]string { + if cr == nil { + return nil + } + return cr.Annotations +} + +// WalkClustersFullPath walks clusters with full path +func (cr *ClickHouseKeeperInstallation) WalkClustersFullPath( + f func(chi *ClickHouseKeeperInstallation, clusterIndex int, cluster *Cluster) error, +) []error { + if cr == nil { + return nil + } + res := make([]error, 0) + + for clusterIndex := range cr.GetSpecT().Configuration.Clusters { + res = append(res, f(cr, clusterIndex, cr.GetSpecT().Configuration.Clusters[clusterIndex])) + } + + return res +} + +// WalkClusters walks clusters +func (cr *ClickHouseKeeperInstallation) WalkClusters(f func(i apiChi.ICluster) error) []error { + if cr == nil { + return nil + } + res := make([]error, 0) + + for clusterIndex := range cr.GetSpecT().Configuration.Clusters { + res = append(res, f(cr.GetSpecT().Configuration.Clusters[clusterIndex])) + } + + return res +} + +// WalkShards walks shards +func (cr *ClickHouseKeeperInstallation) WalkShards( + f func( + shard *ChkShard, + ) error, +) []error { + if cr == nil { + return nil + } + res := make([]error, 0) + + for clusterIndex := range cr.GetSpecT().Configuration.Clusters { + cluster := cr.GetSpecT().Configuration.Clusters[clusterIndex] + for shardIndex := range cluster.Layout.Shards { + shard := cluster.Layout.Shards[shardIndex] + res = append(res, f(shard)) + } + } + + return res +} + +// WalkHostsFullPathAndScope walks hosts with full path +func (cr *ClickHouseKeeperInstallation) WalkHostsFullPathAndScope( + crScopeCycleSize int, + clusterScopeCycleSize int, + f apiChi.WalkHostsAddressFn, +) (res []error) { + if cr == nil { + return nil + } + address := types.NewHostScopeAddress(crScopeCycleSize, clusterScopeCycleSize) + for clusterIndex := range cr.GetSpecT().Configuration.Clusters { + cluster := cr.GetSpecT().Configuration.Clusters[clusterIndex] + address.ClusterScopeAddress.Init() + for shardIndex := range cluster.Layout.Shards { + shard := cluster.GetShard(shardIndex) + for replicaIndex, host := range shard.Hosts { + replica := cluster.GetReplica(replicaIndex) + address.ClusterIndex = clusterIndex + address.ShardIndex = shardIndex + address.ReplicaIndex = replicaIndex + res = append(res, f(cr, cluster, shard, replica, host, address)) + address.CRScopeAddress.Inc() + address.ClusterScopeAddress.Inc() + } + } + } + return res +} + +// WalkHostsFullPath walks hosts with a function +func (cr *ClickHouseKeeperInstallation) WalkHostsFullPath(f apiChi.WalkHostsAddressFn) []error { + return cr.WalkHostsFullPathAndScope(0, 0, f) +} + +// WalkHosts walks hosts with a function +func (cr *ClickHouseKeeperInstallation) WalkHosts(f func(host *apiChi.Host) error) []error { + if cr == nil { + return nil + } + res := make([]error, 0) + + for clusterIndex := range cr.GetSpecT().Configuration.Clusters { + cluster := cr.GetSpecT().Configuration.Clusters[clusterIndex] + for shardIndex := range cluster.Layout.Shards { + shard := cluster.Layout.Shards[shardIndex] + for replicaIndex := range shard.Hosts { + host := shard.Hosts[replicaIndex] + res = append(res, f(host)) + } + } + } + + return res +} + +// WalkTillError walks hosts with a function until an error met +func (cr *ClickHouseKeeperInstallation) WalkTillError( + ctx context.Context, + fCRPreliminary func(ctx context.Context, chi *ClickHouseKeeperInstallation) error, + fCluster func(ctx context.Context, cluster *Cluster) error, + fCRFinal func(ctx context.Context, chi *ClickHouseKeeperInstallation) error, +) error { + if err := fCRPreliminary(ctx, cr); err != nil { + return err + } + + for clusterIndex := range cr.GetSpecT().Configuration.Clusters { + cluster := cr.GetSpecT().Configuration.Clusters[clusterIndex] + if err := fCluster(ctx, cluster); err != nil { + return err + } + } + + if err := fCRFinal(ctx, cr); err != nil { + return err + } + + return nil +} + +func (cr *ClickHouseKeeperInstallation) IsZero() bool { + return cr == nil +} + +func (cr *ClickHouseKeeperInstallation) IsNonZero() bool { + return cr != nil +} + +func (cr *ClickHouseKeeperInstallation) NamespaceName() (string, string) { + return util.NamespaceName(cr) +} + +func (cr *ClickHouseKeeperInstallation) FindMinMaxVersions() { + cr.runtime.MinVersion = swversion.MaxVersion() + cr.runtime.MaxVersion = swversion.MinVersion() + cr.WalkHosts(func(host *apiChi.Host) error { + if host.Runtime.Version.Cmp(cr.runtime.MinVersion) < 0 { + cr.runtime.MinVersion = host.Runtime.Version + } + if host.Runtime.Version.Cmp(cr.runtime.MaxVersion) > 0 { + cr.runtime.MaxVersion = host.Runtime.Version + } + return nil + }) +} + +func (cr *ClickHouseKeeperInstallation) GetMinVersion() *swversion.SoftWareVersion { + return cr.runtime.MinVersion +} + +func (cr *ClickHouseKeeperInstallation) GetMaxVersion() *swversion.SoftWareVersion { + return cr.runtime.MaxVersion +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_cluster.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_cluster.go new file mode 100644 index 00000000..8f877530 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_cluster.go @@ -0,0 +1,434 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + apiChi "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +// Cluster defines item of a clusters section of .configuration +type Cluster struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Settings *apiChi.Settings `json:"settings,omitempty" yaml:"settings,omitempty"` + Files *apiChi.Settings `json:"files,omitempty" yaml:"files,omitempty"` + Templates *apiChi.TemplatesList `json:"templates,omitempty" yaml:"templates,omitempty"` + PDBManaged *types.StringBool `json:"pdbManaged,omitempty" yaml:"pdbManaged,omitempty"` + PDBMaxUnavailable *types.Int32 `json:"pdbMaxUnavailable,omitempty" yaml:"pdbMaxUnavailable,omitempty"` + Reconcile *apiChi.ClusterReconcile `json:"reconcile,omitempty" yaml:"reconcile,omitempty"` + Layout *ChkClusterLayout `json:"layout,omitempty" yaml:"layout,omitempty"` + + Runtime ChkClusterRuntime `json:"-" yaml:"-"` +} + +type ChkClusterRuntime struct { + Address ChkClusterAddress `json:"-" yaml:"-"` + CHK *ClickHouseKeeperInstallation `json:"-" yaml:"-" testdiff:"ignore"` +} + +func (r *ChkClusterRuntime) GetAddress() apiChi.IClusterAddress { + return &r.Address +} + +func (r ChkClusterRuntime) GetCR() apiChi.ICustomResource { + return r.CHK +} + +func (r *ChkClusterRuntime) SetCR(cr apiChi.ICustomResource) { + r.CHK = cr.(*ClickHouseKeeperInstallation) +} + +// ChkClusterAddress defines address of a cluster within ClickHouseInstallation +type ChkClusterAddress struct { + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + CHIName string `json:"chiName,omitempty" yaml:"chiName,omitempty"` + ClusterName string `json:"clusterName,omitempty" yaml:"clusterName,omitempty"` + ClusterIndex int `json:"clusterIndex,omitempty" yaml:"clusterIndex,omitempty"` +} + +func (a *ChkClusterAddress) GetNamespace() string { + return a.Namespace +} + +func (a *ChkClusterAddress) SetNamespace(namespace string) { + a.Namespace = namespace +} + +func (a *ChkClusterAddress) GetCRName() string { + return a.CHIName +} + +func (a *ChkClusterAddress) SetCRName(name string) { + a.CHIName = name +} + +func (a *ChkClusterAddress) GetClusterName() string { + return a.ClusterName +} + +func (a *ChkClusterAddress) SetClusterName(name string) { + a.ClusterName = name +} + +func (a *ChkClusterAddress) GetClusterIndex() int { + return a.ClusterIndex +} + +func (a *ChkClusterAddress) SetClusterIndex(index int) { + a.ClusterIndex = index +} + +func (cluster *Cluster) GetName() string { + return cluster.Name +} + +// HasName checks whether cluster has a name +func (cluster *Cluster) HasName() bool { + if cluster == nil { + return false + } + return len(cluster.GetName()) > 0 +} + +func (c *Cluster) GetZookeeper() *apiChi.ZookeeperConfig { + return nil +} + +func (c *Cluster) GetSchemaPolicy() *apiChi.SchemaPolicy { + return nil +} + +// GetInsecure is a getter +func (cluster *Cluster) GetInsecure() *types.StringBool { + return nil +} + +// GetSecure is a getter +func (cluster *Cluster) GetSecure() *types.StringBool { + return nil +} + +// GetSecret is a getter +func (c *Cluster) GetSecret() *apiChi.ClusterSecret { + return nil +} + +// GetPDBManaged is a getter +func (cluster *Cluster) GetPDBManaged() *types.StringBool { + return cluster.PDBManaged +} + +// GetPDBMaxUnavailable is a getter +func (cluster *Cluster) GetPDBMaxUnavailable() *types.Int32 { + return cluster.PDBMaxUnavailable +} + +// GetReconcile is a getter +func (cluster *Cluster) GetReconcile() *apiChi.ClusterReconcile { + cluster.Reconcile = cluster.Reconcile.Ensure() + return cluster.Reconcile +} + +// GetRuntime is a getter +func (cluster *Cluster) GetRuntime() apiChi.IClusterRuntime { + return &cluster.Runtime +} + +// FillShardsReplicasExplicitlySpecified fills whether shard or replicas are explicitly specified +func (cluster *Cluster) FillShardsReplicasExplicitlySpecified() { + if len(cluster.Layout.Shards) > 0 { + cluster.Layout.ShardsExplicitlySpecified = true + } + if len(cluster.Layout.Replicas) > 0 { + cluster.Layout.ReplicasExplicitlySpecified = true + } +} + +// isShardExplicitlySpecified checks whether shard is explicitly specified +func (cluster *Cluster) isShardExplicitlySpecified() bool { + return cluster.Layout.ShardsExplicitlySpecified +} + +// isReplicaExplicitlySpecified checks whether replica is explicitly specified +func (cluster *Cluster) isReplicaExplicitlySpecified() bool { + return cluster.Layout.ReplicasExplicitlySpecified && !cluster.isShardExplicitlySpecified() +} + +// IsShardSpecified checks whether shard is explicitly specified +func (cluster *Cluster) isShardToBeUsedToInheritSettingsFrom() bool { + if !cluster.isShardExplicitlySpecified() && !cluster.isReplicaExplicitlySpecified() { + return true + } + + return cluster.isShardExplicitlySpecified() +} + +func (cluster *Cluster) SelectSettingsSourceFrom(shard apiChi.IShard, replica apiChi.IReplica) any { + if cluster.isShardToBeUsedToInheritSettingsFrom() { + return shard + } + return replica +} + +// InheritFilesFrom inherits files from CR +func (cluster *Cluster) InheritFilesFrom(chk *ClickHouseKeeperInstallation) { + if chk.GetSpecT().Configuration == nil { + return + } + if chk.GetSpecT().Configuration.Files == nil { + return + } + + // Propagate host section only + cluster.Files = cluster.Files.MergeFromCB(chk.GetSpecT().Configuration.Files, func(path string, _ *apiChi.Setting) bool { + if section, err := apiChi.GetSectionFromPath(path); err == nil { + if section.Equal(apiChi.SectionHost) { + return true + } + } + + return false + }) +} + +// InheritClusterReconcileFrom inherits reconcile runtime from CHI +func (cluster *Cluster) InheritClusterReconcileFrom(chk *ClickHouseKeeperInstallation) { + if chk.Spec.Reconcile == nil { + return + } + reconcile := cluster.GetReconcile() + reconcile.Runtime = reconcile.Runtime.MergeFrom(chk.Spec.Reconcile.Runtime, apiChi.MergeTypeFillEmptyValues) + reconcile.Host = reconcile.Host.MergeFrom(chk.Spec.Reconcile.Host) + cluster.Reconcile = reconcile +} + +// InheritTemplatesFrom inherits templates from CHI +func (cluster *Cluster) InheritTemplatesFrom(chk *ClickHouseKeeperInstallation) { + if chk.GetSpec().GetDefaults() == nil { + return + } + if chk.GetSpec().GetDefaults().Templates == nil { + return + } + cluster.Templates = cluster.Templates.MergeFrom(chk.GetSpec().GetDefaults().Templates, apiChi.MergeTypeFillEmptyValues) + cluster.Templates.HandleDeprecatedFields() +} + +// GetServiceTemplate returns service template, if exists +func (cluster *Cluster) GetServiceTemplate() (*apiChi.ServiceTemplate, bool) { + return nil, false +} + +// GetCR gets parent CR +func (cluster *Cluster) GetCR() *ClickHouseKeeperInstallation { + return cluster.Runtime.CHK +} + +func (cluster *Cluster) GetAncestor() apiChi.ICluster { + return (*Cluster)(nil) +} + +// GetShard gets shard with specified index +func (cluster *Cluster) GetShard(shard int) *ChkShard { + return cluster.Layout.Shards[shard] +} + +// GetOrCreateHost gets or creates host on specified coordinates +func (cluster *Cluster) GetOrCreateHost(shard, replica int) *apiChi.Host { + return cluster.Layout.HostsField.GetOrCreate(shard, replica) +} + +// GetReplica gets replica with specified index +func (cluster *Cluster) GetReplica(replica int) *ChkReplica { + return cluster.Layout.Replicas[replica] +} + +// FindShard finds shard by name or index. +// Expectations: name is expected to be a string, index is expected to be an int. +func (cluster *Cluster) FindShard(needle interface{}) apiChi.IShard { + var resultShard *ChkShard + cluster.WalkShards(func(index int, shard apiChi.IShard) error { + switch v := needle.(type) { + case string: + if shard.GetName() == v { + resultShard = shard.(*ChkShard) + } + case int: + if index == v { + resultShard = shard.(*ChkShard) + } + } + return nil + }) + return resultShard +} + +// FindHost finds host by name or index. +// Expectations: name is expected to be a string, index is expected to be an int. +func (cluster *Cluster) FindHost(needleShard interface{}, needleHost interface{}) *apiChi.Host { + return cluster.FindShard(needleShard).FindHost(needleHost) +} + +// FirstHost finds first host in the cluster +func (cluster *Cluster) FirstHost() *apiChi.Host { + var result *apiChi.Host + cluster.WalkHosts(func(host *apiChi.Host) error { + if result == nil { + result = host + } + return nil + }) + return result +} + +// WalkShards walks shards +func (cluster *Cluster) WalkShards(f func(index int, shard apiChi.IShard) error) []error { + if cluster == nil { + return nil + } + res := make([]error, 0) + + for shardIndex := range cluster.Layout.Shards { + shard := cluster.Layout.Shards[shardIndex] + res = append(res, f(shardIndex, shard)) + } + + return res +} + +// WalkReplicas walks replicas +func (cluster *Cluster) WalkReplicas(f func(index int, replica *ChkReplica) error) []error { + res := make([]error, 0) + + for replicaIndex := range cluster.Layout.Replicas { + replica := cluster.Layout.Replicas[replicaIndex] + res = append(res, f(replicaIndex, replica)) + } + + return res +} + +// WalkHosts walks hosts +func (cluster *Cluster) WalkHosts(f func(host *apiChi.Host) error) []error { + res := make([]error, 0) + + for shardIndex := range cluster.Layout.Shards { + shard := cluster.Layout.Shards[shardIndex] + for replicaIndex := range shard.Hosts { + host := shard.Hosts[replicaIndex] + res = append(res, f(host)) + } + } + + return res +} + +// WalkHostsByShards walks hosts by shards +func (cluster *Cluster) WalkHostsByShards(f func(shard, replica int, host *apiChi.Host) error) []error { + + res := make([]error, 0) + + for shardIndex := range cluster.Layout.Shards { + shard := cluster.Layout.Shards[shardIndex] + for replicaIndex := range shard.Hosts { + host := shard.Hosts[replicaIndex] + res = append(res, f(shardIndex, replicaIndex, host)) + } + } + + return res +} + +func (cluster *Cluster) GetLayout() *ChkClusterLayout { + return cluster.Layout +} + +// WalkHostsByReplicas walks hosts by replicas +func (cluster *Cluster) WalkHostsByReplicas(f func(shard, replica int, host *apiChi.Host) error) []error { + + res := make([]error, 0) + + for replicaIndex := range cluster.Layout.Replicas { + replica := cluster.Layout.Replicas[replicaIndex] + for shardIndex := range replica.Hosts { + host := replica.Hosts[shardIndex] + res = append(res, f(shardIndex, replicaIndex, host)) + } + } + + return res +} + +// HostsCount counts hosts +func (cluster *Cluster) HostsCount() int { + count := 0 + cluster.WalkHosts(func(host *apiChi.Host) error { + count++ + return nil + }) + return count +} + +func (cluster *Cluster) IsZero() bool { + return cluster == nil +} + +func (cluster *Cluster) IsNonZero() bool { + return cluster != nil +} + +// IsStopped checks whether host is stopped +func (cluster *Cluster) IsStopped() bool { + return cluster.GetCR().IsStopped() +} + +func (cluster *Cluster) Ensure(create func() *Cluster) *Cluster { + if cluster == nil { + cluster = create() + } + return cluster +} + +// ChkClusterLayout defines layout section of .spec.configuration.clusters +type ChkClusterLayout struct { + ShardsCount int `json:"shardsCount,omitempty" yaml:"shardsCount,omitempty"` + ReplicasCount int `json:"replicasCount,omitempty" yaml:"replicasCount,omitempty"` + + // TODO refactor into map[string]ChiShard + Shards []*ChkShard `json:"shards,omitempty" yaml:"shards,omitempty"` + Replicas []*ChkReplica `json:"replicas,omitempty" yaml:"replicas,omitempty"` + + // Internal data + // Whether shards or replicas are explicitly specified as Shards []ChiShard or Replicas []ChiReplica + ShardsExplicitlySpecified bool `json:"-" yaml:"-" testdiff:"ignore"` + ReplicasExplicitlySpecified bool `json:"-" yaml:"-" testdiff:"ignore"` + HostsField *apiChi.HostsField `json:"-" yaml:"-" testdiff:"ignore"` +} + +// NewChiClusterLayout creates new cluster layout +func NewChkClusterLayout() *ChkClusterLayout { + return new(ChkClusterLayout) +} + +func (l *ChkClusterLayout) GetReplicasCount() int { + return l.ReplicasCount +} + +func (l *ChkClusterLayout) Ensure() *ChkClusterLayout { + if l == nil { + l = NewChkClusterLayout() + } + return l +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_configuration.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_configuration.go new file mode 100644 index 00000000..da7561ab --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_configuration.go @@ -0,0 +1,82 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + apiChi "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" +) + +// Configuration defines configuration section of .spec +type Configuration struct { + Settings *apiChi.Settings `json:"settings,omitempty" yaml:"settings,omitempty"` + Files *apiChi.Settings `json:"files,omitempty" yaml:"files,omitempty"` + Clusters []*Cluster `json:"clusters,omitempty" yaml:"clusters,omitempty"` +} + +// NewConfiguration creates new ChkConfiguration objects +func NewConfiguration() *Configuration { + return new(Configuration) +} + +func (c *Configuration) Ensure() *Configuration { + if c == nil { + c = NewConfiguration() + } + return c +} + +func (c *Configuration) GetUsers() *apiChi.Settings { + return nil +} + +func (c *Configuration) GetProfiles() *apiChi.Settings { + return nil +} + +func (c *Configuration) GetQuotas() *apiChi.Settings { + return nil +} + +func (c *Configuration) GetSettings() *apiChi.Settings { + if c == nil { + return nil + } + return c.Settings +} + +func (c *Configuration) GetFiles() *apiChi.Settings { + if c == nil { + return nil + } + return c.Files +} + +// MergeFrom merges from specified source +func (c *Configuration) MergeFrom(from *Configuration, _type apiChi.MergeType) *Configuration { + if from == nil { + return c + } + + c = c.Ensure() + + c.Settings = c.Settings.MergeFrom(from.Settings) + c.Files = c.Files.MergeFrom(from.Files) + + // TODO merge clusters + // Copy Clusters for now + c.Clusters = from.Clusters + + return c +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_replica.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_replica.go new file mode 100644 index 00000000..af7f1705 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_replica.go @@ -0,0 +1,202 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import apiChi "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + +// ChiReplica defines item of a replica section of .spec.configuration.clusters[n].replicas +// TODO unify with ChiShard based on HostsSet +type ChkReplica struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Settings *apiChi.Settings `json:"settings,omitempty" yaml:"settings,omitempty"` + Files *apiChi.Settings `json:"files,omitempty" yaml:"files,omitempty"` + Templates *apiChi.TemplatesList `json:"templates,omitempty" yaml:"templates,omitempty"` + ShardsCount int `json:"shardsCount,omitempty" yaml:"shardsCount,omitempty"` + // TODO refactor into map[string]Host + Hosts []*apiChi.Host `json:"shards,omitempty" yaml:"shards,omitempty"` + + Runtime ChkReplicaRuntime `json:"-" yaml:"-"` +} + +type ChkReplicaRuntime struct { + Address ChkReplicaAddress `json:"-" yaml:"-"` + CHK *ClickHouseKeeperInstallation `json:"-" yaml:"-" testdiff:"ignore"` +} + +func (r ChkReplicaRuntime) GetAddress() apiChi.IReplicaAddress { + return &r.Address +} + +func (r *ChkReplicaRuntime) SetCR(cr apiChi.ICustomResource) { + r.CHK = cr.(*ClickHouseKeeperInstallation) +} + +func (replica *ChkReplica) GetName() string { + return replica.Name +} + +// InheritSettingsFrom inherits settings from specified cluster +func (replica *ChkReplica) InheritSettingsFrom(cluster *Cluster) { + replica.Settings = replica.Settings.MergeFrom(cluster.Settings) +} + +// InheritFilesFrom inherits files from specified cluster +func (replica *ChkReplica) InheritFilesFrom(cluster *Cluster) { + replica.Files = replica.Files.MergeFrom(cluster.Files) +} + +// InheritTemplatesFrom inherits templates from specified cluster +func (replica *ChkReplica) InheritTemplatesFrom(cluster *Cluster) { + replica.Templates = replica.Templates.MergeFrom(cluster.Templates, apiChi.MergeTypeFillEmptyValues) + replica.Templates.HandleDeprecatedFields() +} + +// GetServiceTemplate gets service template +func (replica *ChkReplica) GetServiceTemplate() (*apiChi.ServiceTemplate, bool) { + if !replica.Templates.HasReplicaServiceTemplate() { + return nil, false + } + name := replica.Templates.GetReplicaServiceTemplate() + return replica.Runtime.CHK.GetServiceTemplate(name) +} + +// HasShardsCount checks whether replica has shards count specified +func (replica *ChkReplica) HasShardsCount() bool { + if replica == nil { + return false + } + + return replica.ShardsCount > 0 +} + +// WalkHosts walks over hosts +func (replica *ChkReplica) WalkHosts(f func(host *apiChi.Host) error) []error { + res := make([]error, 0) + + for shardIndex := range replica.Hosts { + host := replica.Hosts[shardIndex] + res = append(res, f(host)) + } + + return res +} + +// HostsCount returns number of hosts +func (replica *ChkReplica) HostsCount() int { + count := 0 + replica.WalkHosts(func(host *apiChi.Host) error { + count++ + return nil + }) + return count +} + +func (replica *ChkReplica) HasSettings() bool { + return replica.GetSettings() != nil +} + +func (replica *ChkReplica) GetSettings() *apiChi.Settings { + if replica == nil { + return nil + } + return replica.Settings +} + +func (replica *ChkReplica) HasFiles() bool { + return replica.GetFiles() != nil +} + +func (replica *ChkReplica) GetFiles() *apiChi.Settings { + if replica == nil { + return nil + } + return replica.Files +} + +func (replica *ChkReplica) HasTemplates() bool { + return replica.GetTemplates() != nil +} + +func (replica *ChkReplica) GetTemplates() *apiChi.TemplatesList { + if replica == nil { + return nil + } + return replica.Templates +} + +func (replica *ChkReplica) GetRuntime() apiChi.IReplicaRuntime { + if replica == nil { + return (*ChkReplicaRuntime)(nil) + } + return &replica.Runtime +} + +// ChiReplicaAddress defines address of a replica within ClickHouseInstallation +type ChkReplicaAddress struct { + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + CHIName string `json:"chiName,omitempty" yaml:"chiName,omitempty"` + ClusterName string `json:"clusterName,omitempty" yaml:"clusterName,omitempty"` + ClusterIndex int `json:"clusterIndex,omitempty" yaml:"clusterIndex,omitempty"` + ReplicaName string `json:"replicaName,omitempty" yaml:"replicaName,omitempty"` + ReplicaIndex int `json:"replicaIndex,omitempty" yaml:"replicaIndex,omitempty"` +} + +func (a *ChkReplicaAddress) GetNamespace() string { + return a.Namespace +} + +func (a *ChkReplicaAddress) SetNamespace(namespace string) { + a.Namespace = namespace +} + +func (a *ChkReplicaAddress) GetCRName() string { + return a.CHIName +} + +func (a *ChkReplicaAddress) SetCRName(name string) { + a.CHIName = name +} + +func (a *ChkReplicaAddress) GetClusterName() string { + return a.ClusterName +} + +func (a *ChkReplicaAddress) SetClusterName(name string) { + a.ClusterName = name +} + +func (a *ChkReplicaAddress) GetClusterIndex() int { + return a.ClusterIndex +} + +func (a *ChkReplicaAddress) SetClusterIndex(index int) { + a.ClusterIndex = index +} + +func (a *ChkReplicaAddress) GetReplicaName() string { + return a.ReplicaName +} + +func (a *ChkReplicaAddress) SetReplicaName(name string) { + a.ReplicaName = name +} + +func (a *ChkReplicaAddress) GetReplicaIndex() int { + return a.ReplicaIndex +} + +func (a *ChkReplicaAddress) SetReplicaIndex(index int) { + a.ReplicaIndex = index +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_shard.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_shard.go new file mode 100644 index 00000000..34333e8b --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_shard.go @@ -0,0 +1,310 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + apiChi "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +// ChiShard defines item of a shard section of .spec.configuration.clusters[n].shards +// TODO unify with ChiReplica based on HostsSet +type ChkShard struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Weight *int `json:"weight,omitempty" yaml:"weight,omitempty"` + InternalReplication *types.StringBool `json:"internalReplication,omitempty" yaml:"internalReplication,omitempty"` + Settings *apiChi.Settings `json:"settings,omitempty" yaml:"settings,omitempty"` + Files *apiChi.Settings `json:"files,omitempty" yaml:"files,omitempty"` + Templates *apiChi.TemplatesList `json:"templates,omitempty" yaml:"templates,omitempty"` + ReplicasCount int `json:"replicasCount,omitempty" yaml:"replicasCount,omitempty"` + // TODO refactor into map[string]Host + Hosts []*apiChi.Host `json:"replicas,omitempty" yaml:"replicas,omitempty"` + + Runtime ChkShardRuntime `json:"-" yaml:"-"` + + // DefinitionType is DEPRECATED - to be removed soon + DefinitionType string `json:"definitionType,omitempty" yaml:"definitionType,omitempty"` +} + +type ChkShardRuntime struct { + Address ChkShardAddress `json:"-" yaml:"-"` + CHK *ClickHouseKeeperInstallation `json:"-" yaml:"-" testdiff:"ignore"` +} + +func (r ChkShardRuntime) GetAddress() apiChi.IShardAddress { + return &r.Address +} + +func (r *ChkShardRuntime) GetCR() apiChi.ICustomResource { + return r.CHK +} + +func (r *ChkShardRuntime) SetCR(cr apiChi.ICustomResource) { + r.CHK = cr.(*ClickHouseKeeperInstallation) +} + +func (shard *ChkShard) GetName() string { + return shard.Name +} + +func (shard *ChkShard) GetInternalReplication() *types.StringBool { + return shard.InternalReplication +} + +// InheritSettingsFrom inherits settings from specified cluster +func (shard *ChkShard) InheritSettingsFrom(cluster *Cluster) { + shard.Settings = shard.Settings.MergeFrom(cluster.Settings) +} + +// InheritFilesFrom inherits files from specified cluster +func (shard *ChkShard) InheritFilesFrom(cluster *Cluster) { + shard.Files = shard.Files.MergeFrom(cluster.Files) +} + +// InheritTemplatesFrom inherits templates from specified cluster +func (shard *ChkShard) InheritTemplatesFrom(cluster *Cluster) { + shard.Templates = shard.Templates.MergeFrom(cluster.Templates, apiChi.MergeTypeFillEmptyValues) + shard.Templates.HandleDeprecatedFields() +} + +// GetServiceTemplate gets service template +func (shard *ChkShard) GetServiceTemplate() (*apiChi.ServiceTemplate, bool) { + if !shard.Templates.HasShardServiceTemplate() { + return nil, false + } + name := shard.Templates.GetShardServiceTemplate() + return shard.Runtime.CHK.GetServiceTemplate(name) +} + +// HasReplicasCount checks whether shard has replicas count specified +func (shard *ChkShard) HasReplicasCount() bool { + if shard == nil { + return false + } + + return shard.ReplicasCount > 0 +} + +// WalkHosts runs specified function on each host +func (shard *ChkShard) WalkHosts(f func(host *apiChi.Host) error) []error { + if shard == nil { + return nil + } + + res := make([]error, 0) + + for replicaIndex := range shard.Hosts { + host := shard.Hosts[replicaIndex] + res = append(res, f(host)) + } + + return res +} + +// WalkHosts runs specified function on each host +func (shard *ChkShard) WalkHostsAbortOnError(f func(host *apiChi.Host) error) error { + if shard == nil { + return nil + } + + for replicaIndex := range shard.Hosts { + host := shard.Hosts[replicaIndex] + if err := f(host); err != nil { + return err + } + } + + return nil +} + +// FindHost finds host by name or index. +// Expectations: name is expected to be a string, index is expected to be an int. +func (shard *ChkShard) FindHost(needle interface{}) (res *apiChi.Host) { + shard.WalkHosts(func(host *apiChi.Host) error { + switch v := needle.(type) { + case string: + if host.Runtime.Address.HostName == v { + res = host + } + case int: + if host.Runtime.Address.ShardScopeIndex == v { + res = host + } + } + return nil + }) + return +} + +// FirstHost finds first host in the shard +func (shard *ChkShard) FirstHost() *apiChi.Host { + var result *apiChi.Host + shard.WalkHosts(func(host *apiChi.Host) error { + if result == nil { + result = host + } + return nil + }) + return result +} + +// HostsCount returns count of hosts in the shard +func (shard *ChkShard) HostsCount() int { + count := 0 + shard.WalkHosts(func(host *apiChi.Host) error { + count++ + return nil + }) + return count +} + +// GetCHK gets Custom Resource of the shard +func (shard *ChkShard) GetCHK() *ClickHouseKeeperInstallation { + return shard.Runtime.CHK +} + +func (shard *ChkShard) GetAncestor() apiChi.IShard { + return (*ChkShard)(nil) +} + +// GetCluster gets cluster of the shard +func (shard *ChkShard) GetCluster() *Cluster { + return shard.Runtime.CHK.GetSpecT().Configuration.Clusters[shard.Runtime.Address.ClusterIndex] +} + +// HasWeight checks whether shard has applicable weight value specified +func (shard *ChkShard) HasWeight() bool { + if shard == nil { + return false + } + if shard.Weight == nil { + return false + } + return *shard.Weight >= 0 +} + +// GetWeight gets weight +func (shard *ChkShard) GetWeight() int { + if shard.HasWeight() { + return *shard.Weight + } + return 0 +} + +func (shard *ChkShard) GetRuntime() apiChi.IShardRuntime { + if shard == nil { + return (*ChkShardRuntime)(nil) + } + return &shard.Runtime +} + +func (shard *ChkShard) HasSettings() bool { + return shard.GetSettings() != nil +} + +func (shard *ChkShard) GetSettings() *apiChi.Settings { + if shard == nil { + return nil + } + return shard.Settings +} + +func (shard *ChkShard) HasFiles() bool { + return shard.GetFiles() != nil +} + +func (shard *ChkShard) GetFiles() *apiChi.Settings { + if shard == nil { + return nil + } + return shard.Files +} + +func (shard *ChkShard) HasTemplates() bool { + return shard.GetTemplates() != nil +} + +func (shard *ChkShard) GetTemplates() *apiChi.TemplatesList { + if shard == nil { + return nil + } + return shard.Templates +} + +func (shard *ChkShard) IsZero() bool { + return shard == nil +} + +func (shard *ChkShard) IsNonZero() bool { + return shard != nil +} + +// ChiShardAddress defines address of a shard within ClickHouseInstallation +type ChkShardAddress struct { + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + CHIName string `json:"chiName,omitempty" yaml:"chiName,omitempty"` + ClusterName string `json:"clusterName,omitempty" yaml:"clusterName,omitempty"` + ClusterIndex int `json:"clusterIndex,omitempty" yaml:"clusterIndex,omitempty"` + ShardName string `json:"shardName,omitempty" yaml:"shardName,omitempty"` + ShardIndex int `json:"shardIndex,omitempty" yaml:"shardIndex,omitempty"` +} + +func (a *ChkShardAddress) GetNamespace() string { + return a.Namespace +} + +func (a *ChkShardAddress) SetNamespace(namespace string) { + a.Namespace = namespace +} + +func (a *ChkShardAddress) GetCRName() string { + return a.CHIName +} + +func (a *ChkShardAddress) SetCRName(name string) { + a.CHIName = name +} + +func (a *ChkShardAddress) GetClusterName() string { + return a.ClusterName +} + +func (a *ChkShardAddress) SetClusterName(name string) { + a.ClusterName = name +} + +func (a *ChkShardAddress) GetClusterIndex() int { + return a.ClusterIndex +} + +func (a *ChkShardAddress) SetClusterIndex(index int) { + a.ClusterIndex = index +} + +func (a *ChkShardAddress) GetShardName() string { + return a.ShardName +} + +func (a *ChkShardAddress) SetShardName(name string) { + a.ShardName = name +} + +func (a *ChkShardAddress) GetShardIndex() int { + return a.ShardIndex +} + +func (a *ChkShardAddress) SetShardIndex(index int) { + a.ShardIndex = index +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_spec.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_spec.go new file mode 100644 index 00000000..5d620cc3 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_spec.go @@ -0,0 +1,130 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + apiChi "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +// ChkSpec defines spec section of ClickHouseKeeper resource +type ChkSpec struct { + TaskID *types.Id `json:"taskID,omitempty" yaml:"taskID,omitempty"` + Stop *types.StringBool `json:"stop,omitempty" yaml:"stop,omitempty"` + NamespaceDomainPattern *types.String `json:"namespaceDomainPattern,omitempty" yaml:"namespaceDomainPattern,omitempty"` + Suspend *types.StringBool `json:"suspend,omitempty" yaml:"suspend,omitempty"` + Reconciling *apiChi.ChiReconcile `json:"reconciling,omitempty" yaml:"reconciling,omitempty"` + Reconcile *apiChi.ChiReconcile `json:"reconcile,omitempty" yaml:"reconcile,omitempty"` + Defaults *apiChi.Defaults `json:"defaults,omitempty" yaml:"defaults,omitempty"` + Configuration *Configuration `json:"configuration,omitempty" yaml:"configuration,omitempty"` + Templates *apiChi.Templates `json:"templates,omitempty" yaml:"templates,omitempty"` +} + +// HasTaskID checks whether task id is specified +func (spec *ChkSpec) HasTaskID() bool { + if spec == nil { + return false + } + return spec.TaskID.HasValue() +} + +// GetTaskID gets task id as a string +func (spec *ChkSpec) GetTaskID() *types.Id { + if spec == nil { + return nil + } + return spec.TaskID +} + +func (spec *ChkSpec) GetStop() *types.StringBool { + if spec == nil { + return (*types.StringBool)(nil) + } + return spec.Stop +} + +func (spec *ChkSpec) GetNamespaceDomainPattern() *types.String { + if spec == nil { + return (*types.String)(nil) + } + return spec.NamespaceDomainPattern +} + +func (spec *ChkSpec) GetDefaults() *apiChi.Defaults { + if spec == nil { + return (*apiChi.Defaults)(nil) + } + return spec.Defaults +} + +func (spec *ChkSpec) GetConfiguration() apiChi.IConfiguration { + if spec == nil { + return (*Configuration)(nil) + } + return spec.Configuration +} + +func (spec *ChkSpec) GetTemplates() *apiChi.Templates { + if spec == nil { + return (*apiChi.Templates)(nil) + } + return spec.Templates +} + +// MergeFrom merges from spec +func (spec *ChkSpec) MergeFrom(from *ChkSpec, _type apiChi.MergeType) { + if from == nil { + return + } + + if spec == nil { + spec = &ChkSpec{} + } + + switch _type { + case apiChi.MergeTypeFillEmptyValues: + if !spec.HasTaskID() { + spec.TaskID = spec.TaskID.MergeFrom(from.TaskID) + } + if !spec.Stop.HasValue() { + spec.Stop = spec.Stop.MergeFrom(from.Stop) + } + if !spec.NamespaceDomainPattern.HasValue() { + spec.NamespaceDomainPattern = spec.NamespaceDomainPattern.MergeFrom(from.NamespaceDomainPattern) + } + if !spec.Suspend.HasValue() { + spec.Suspend = spec.Suspend.MergeFrom(from.Suspend) + } + case apiChi.MergeTypeOverrideByNonEmptyValues: + if from.HasTaskID() { + spec.TaskID = spec.TaskID.MergeFrom(from.TaskID) + } + if from.Stop.HasValue() { + // Override by non-empty values only + spec.Stop = from.Stop + } + if from.NamespaceDomainPattern.HasValue() { + spec.NamespaceDomainPattern = spec.NamespaceDomainPattern.MergeFrom(from.NamespaceDomainPattern) + } + if spec.Suspend.HasValue() { + spec.Suspend = spec.Suspend.MergeFrom(from.Suspend) + } + } + + spec.Reconcile = spec.Reconcile.MergeFrom(from.Reconcile, _type) + spec.Defaults = spec.Defaults.MergeFrom(from.Defaults, _type) + spec.Configuration = spec.Configuration.MergeFrom(from.Configuration, _type) + spec.Templates = spec.Templates.MergeFrom(from.Templates, _type) +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_status.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_status.go new file mode 100644 index 00000000..486abce3 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/type_status.go @@ -0,0 +1,899 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "sort" + "sync" + + chi "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/version" +) + +const ( + maxActions = 10 + maxErrors = 10 + maxTaskIDs = 10 +) + +// Possible CR statuses +const ( + StatusInProgress = "InProgress" + StatusCompleted = "Completed" + StatusAborted = "Aborted" + StatusTerminating = "Terminating" +) + +// Status defines status section of the custom resource. +// +// Note: application level reads and writes to Status fields should be done through synchronized getter/setter functions. +// While all of these fields need to be exported for JSON and YAML serialization/deserialization, we can at least audit +// that application logic sticks to the synchronized getter/setters by auditing whether all explicit Go field-level +// accesses are strictly within _this_ source file OR the generated deep copy source file. +type Status struct { + CHOpVersion string `json:"chop-version,omitempty" yaml:"chop-version,omitempty"` + CHOpCommit string `json:"chop-commit,omitempty" yaml:"chop-commit,omitempty"` + CHOpDate string `json:"chop-date,omitempty" yaml:"chop-date,omitempty"` + CHOpIP string `json:"chop-ip,omitempty" yaml:"chop-ip,omitempty"` + ClustersCount int `json:"clusters,omitempty" yaml:"clusters,omitempty"` + ShardsCount int `json:"shards,omitempty" yaml:"shards,omitempty"` + ReplicasCount int `json:"replicas,omitempty" yaml:"replicas,omitempty"` + HostsCount int `json:"hosts,omitempty" yaml:"hosts,omitempty"` + Status string `json:"status,omitempty" yaml:"status,omitempty"` + TaskID string `json:"taskID,omitempty" yaml:"taskID,omitempty"` + TaskIDsStarted []string `json:"taskIDsStarted,omitempty" yaml:"taskIDsStarted,omitempty"` + TaskIDsCompleted []string `json:"taskIDsCompleted,omitempty" yaml:"taskIDsCompleted,omitempty"` + Action string `json:"action,omitempty" yaml:"action,omitempty"` + Actions []string `json:"actions,omitempty" yaml:"actions,omitempty"` + Error string `json:"error,omitempty" yaml:"error,omitempty"` + Errors []string `json:"errors,omitempty" yaml:"errors,omitempty"` + HostsUpdatedCount int `json:"hostsUpdated,omitempty" yaml:"hostsUpdated,omitempty"` + HostsAddedCount int `json:"hostsAdded,omitempty" yaml:"hostsAdded,omitempty"` + HostsUnchangedCount int `json:"hostsUnchanged,omitempty" yaml:"hostsUnchanged,omitempty"` + HostsFailedCount int `json:"hostsFailed,omitempty" yaml:"hostsFailed,omitempty"` + HostsCompletedCount int `json:"hostsCompleted,omitempty" yaml:"hostsCompleted,omitempty"` + HostsDeletedCount int `json:"hostsDeleted,omitempty" yaml:"hostsDeleted,omitempty"` + HostsDeleteCount int `json:"hostsDelete,omitempty" yaml:"hostsDelete,omitempty"` + Pods []string `json:"pods,omitempty" yaml:"pods,omitempty"` + PodIPs []string `json:"pod-ips,omitempty" yaml:"pod-ips,omitempty"` + FQDNs []string `json:"fqdns,omitempty" yaml:"fqdns,omitempty"` + Endpoint string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"` + Endpoints []string `json:"endpoints,omitempty" yaml:"endpoints,omitempty"` + NormalizedCR *ClickHouseKeeperInstallation `json:"normalized,omitempty" yaml:"normalized,omitempty"` + NormalizedCRCompleted *ClickHouseKeeperInstallation `json:"normalizedCompleted,omitempty" yaml:"normalizedCompleted,omitempty"` + ActionPlan *chi.ActionPlan `json:"actionPlan,omitempty" yaml:"actionPlan,omitempty"` + HostsWithTablesCreated []string `json:"hostsWithTablesCreated,omitempty" yaml:"hostsWithTablesCreated,omitempty"` + HostsWithReplicaCaughtUp []string `json:"hostsWithReplicaCaughtUp,omitempty" yaml:"hostsWithReplicaCaughtUp,omitempty"` + UsedTemplates []*chi.TemplateRef `json:"usedTemplates,omitempty" yaml:"usedTemplates,omitempty"` + + mu sync.RWMutex `json:"-" yaml:"-"` +} + +// FillStatusParams is a struct used to fill status params +type FillStatusParams struct { + CHOpIP string + ClustersCount int + ShardsCount int + HostsCount int + TaskID string + HostsUpdatedCount int + HostsAddedCount int + HostsUnchangedCount int + HostsCompletedCount int + HostsDeleteCount int + HostsDeletedCount int + Pods []string + FQDNs []string + Endpoint string + Endpoints []string + NormalizedCR *ClickHouseKeeperInstallation +} + +// Fill is a synchronized setter for a fairly large number of fields. We take a struct type "params" argument to avoid +// confusion of similarly typed positional arguments, and to avoid defining a lot of separate synchronized setters +// for these fields that are typically all set together at once (during "fills"). +func (s *Status) Fill(params *FillStatusParams) { + doWithWriteLock(s, func(s *Status) { + // We always set these (build-hardcoded) version fields. + s.CHOpVersion = version.Version + s.CHOpCommit = version.GitSHA + s.CHOpDate = version.BuiltAt + + // Now, set fields from the provided input. + s.CHOpIP = params.CHOpIP + s.ClustersCount = params.ClustersCount + s.ShardsCount = params.ShardsCount + s.HostsCount = params.HostsCount + s.TaskID = params.TaskID + s.HostsUpdatedCount = params.HostsUpdatedCount + s.HostsAddedCount = params.HostsAddedCount + s.HostsUnchangedCount = params.HostsUnchangedCount + s.HostsCompletedCount = params.HostsCompletedCount + s.HostsDeleteCount = params.HostsDeleteCount + s.HostsDeletedCount = params.HostsDeletedCount + s.Pods = params.Pods + s.FQDNs = util.NormalizeFQDNs(params.FQDNs) + s.Endpoint = util.NormalizeFQDN(params.Endpoint) + s.Endpoints = util.NormalizeFQDNs(params.Endpoints) + s.NormalizedCR = params.NormalizedCR + }) +} + +// SetError sets status error +func (s *Status) SetError(err string) { + doWithWriteLock(s, func(s *Status) { + s.Error = err + }) +} + +// PushError sets and pushes error into status +func (s *Status) PushError(error string) { + doWithWriteLock(s, func(s *Status) { + s.Errors = append([]string{error}, s.Errors...) + if len(s.Errors) > maxErrors { + s.Errors = s.Errors[:maxErrors] + } + }) +} + +// SetAndPushError sets and pushes error into status +func (s *Status) SetAndPushError(err string) { + doWithWriteLock(s, func(s *Status) { + s.Error = err + s.Errors = append([]string{err}, s.Errors...) + if len(s.Errors) > maxErrors { + s.Errors = s.Errors[:maxErrors] + } + }) +} + +// PushHostReplicaCaughtUp pushes host to the list of hosts with replica caught-up +func (s *Status) PushHostReplicaCaughtUp(host string) { + host = util.NormalizeFQDN(host) + doWithWriteLock(s, func(s *Status) { + if util.InArray(host, s.HostsWithReplicaCaughtUp) { + return + } + s.HostsWithReplicaCaughtUp = append(s.HostsWithReplicaCaughtUp, host) + }) +} + +// PushHostTablesCreated pushes host to the list of hosts with created tables +func (s *Status) PushHostTablesCreated(host string) { + host = util.NormalizeFQDN(host) + doWithWriteLock(s, func(s *Status) { + if util.InArray(host, s.HostsWithTablesCreated) { + return + } + s.HostsWithTablesCreated = append(s.HostsWithTablesCreated, host) + }) +} + +// SyncHostTablesCreated syncs list of hosts with tables created with actual list of hosts +func (s *Status) SyncHostTablesCreated() { + doWithWriteLock(s, func(s *Status) { + if s.FQDNs == nil { + return + } + // Normalize both sides to handle status data written by older operator versions + // that may contain trailing dots. + s.HostsWithTablesCreated = util.IntersectStringArrays( + util.NormalizeFQDNs(s.HostsWithTablesCreated), + util.NormalizeFQDNs(s.FQDNs), + ) + }) +} + +// PushUsedTemplate pushes used templates to the list of used templates +func (s *Status) PushUsedTemplate(templateRefs ...*chi.TemplateRef) { + if len(templateRefs) > 0 { + doWithWriteLock(s, func(s *Status) { + s.UsedTemplates = append(s.UsedTemplates, templateRefs...) + }) + } +} + +// GetUsedTemplatesCount gets used templates count +func (s *Status) GetUsedTemplatesCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return len(s.UsedTemplates) + }) +} + +// SetAction action setter +func (s *Status) SetAction(action string) { + doWithWriteLock(s, func(s *Status) { + s.Action = action + }) +} + +// PushAction pushes action into status +func (s *Status) PushAction(action string) { + doWithWriteLock(s, func(s *Status) { + s.Actions = append([]string{action}, s.Actions...) + trimActionsNoSync(s) + }) +} + +// HasNormalizedCRCompleted is a checker +func (s *Status) HasNormalizedCRCompleted() bool { + return s.GetNormalizedCRCompleted() != nil +} + +// HasNormalizedCR is a checker +func (s *Status) HasNormalizedCR() bool { + return s.GetNormalizedCR() != nil +} + +// SetPodIPs sets pod IPs +func (s *Status) SetPodIPs(podIPs []string) { + doWithWriteLock(s, func(s *Status) { + s.PodIPs = podIPs + }) +} + +// HostDeleted increments deleted hosts counter +func (s *Status) HostDeleted() { + doWithWriteLock(s, func(s *Status) { + s.HostsDeletedCount++ + }) +} + +// HostUpdated increments updated hosts counter +func (s *Status) HostUpdated() { + doWithWriteLock(s, func(s *Status) { + s.HostsUpdatedCount++ + }) +} + +// HostAdded increments added hosts counter +func (s *Status) HostAdded() { + doWithWriteLock(s, func(s *Status) { + s.HostsAddedCount++ + }) +} + +// HostUnchanged increments unchanged hosts counter +func (s *Status) HostUnchanged() { + doWithWriteLock(s, func(s *Status) { + s.HostsUnchangedCount++ + }) +} + +// HostFailed increments failed hosts counter +func (s *Status) HostFailed() { + doWithWriteLock(s, func(s *Status) { + s.HostsFailedCount++ + }) +} + +// HostCompleted increments completed hosts counter +func (s *Status) HostCompleted() { + doWithWriteLock(s, func(s *Status) { + s.HostsCompletedCount++ + }) +} + +// ReconcileStart marks reconcile start +func (s *Status) ReconcileStart(ap chi.IActionPlan) { + doWithWriteLock(s, func(s *Status) { + if s == nil { + return + } + s.Status = StatusInProgress + s.HostsUpdatedCount = 0 + s.HostsAddedCount = 0 + s.HostsUnchangedCount = 0 + s.HostsCompletedCount = 0 + s.HostsDeletedCount = 0 + s.HostsDeleteCount = ap.GetRemovedHostsNum() + s.ActionPlan = ap.(*chi.ActionPlan) + pushTaskIDStartedNoSync(s) + }) +} + +// ReconcileComplete marks reconcile completion +func (s *Status) ReconcileComplete() { + doWithWriteLock(s, func(s *Status) { + if s == nil { + return + } + s.Status = StatusCompleted + s.Action = "" + pushTaskIDCompletedNoSync(s) + }) +} + +// ReconcileAbort marks reconcile abortion +func (s *Status) ReconcileAbort() { + doWithWriteLock(s, func(s *Status) { + if s == nil { + return + } + s.Status = StatusAborted + s.Action = "" + pushTaskIDCompletedNoSync(s) + }) +} + +// DeleteStart marks deletion start +func (s *Status) DeleteStart() { + doWithWriteLock(s, func(s *Status) { + if s == nil { + return + } + s.Status = StatusTerminating + s.HostsUpdatedCount = 0 + s.HostsAddedCount = 0 + s.HostsUnchangedCount = 0 + s.HostsCompletedCount = 0 + s.HostsDeletedCount = 0 + s.HostsDeleteCount = 0 + pushTaskIDStartedNoSync(s) + }) +} + +// SetActionPlan sets action plan +func (s *Status) SetActionPlan(ap chi.IActionPlan) { + doWithWriteLock(s, func(s *Status) { + s.ActionPlan = ap.(*chi.ActionPlan) + }) +} + +func prepareOptions(opts types.CopyStatusOptions) types.CopyStatusOptions { + if opts.FieldGroupInheritable { + opts.Copy.TaskIDsStarted = true + opts.Copy.TaskIDsCompleted = true + opts.Copy.Actions = true + opts.Copy.Errors = true + opts.Copy.UsedTemplates = true + } + + if opts.FieldGroupActions { + opts.Copy.Action = true + opts.Merge.Actions = true + opts.Copy.UsedTemplates = true + } + + if opts.FieldGroupErrors { + opts.Copy.Error = true + opts.Merge.Errors = true + } + + if opts.FieldGroupMain { + opts.Copy.CHOpVersion = true + opts.Copy.CHOpCommit = true + opts.Copy.CHOpDate = true + opts.Copy.CHOpIP = true + opts.Copy.ClustersCount = true + opts.Copy.ShardsCount = true + opts.Copy.ReplicasCount = true + opts.Copy.HostsCount = true + opts.Copy.Status = true + opts.Copy.TaskID = true + opts.Copy.TaskIDsStarted = true + opts.Copy.TaskIDsCompleted = true + opts.Copy.Action = true + opts.Merge.Actions = true + opts.Copy.Error = true + opts.Copy.Errors = true + opts.Copy.HostsUpdatedCount = true + opts.Copy.HostsAddedCount = true + opts.Copy.HostsUnchangedCount = true + opts.Copy.HostsCompletedCount = true + opts.Copy.HostsDeletedCount = true + opts.Copy.HostsDeleteCount = true + opts.Copy.Pods = true + opts.Copy.PodIPs = true + opts.Copy.FQDNs = true + opts.Copy.Endpoint = true + opts.Copy.NormalizedCR = true + opts.Copy.ActionPlan = true + opts.Copy.UsedTemplates = true + } + + if opts.FieldGroupNormalized { + opts.Copy.NormalizedCR = true + opts.Copy.ActionPlan = true + } + + if opts.FieldGroupWholeStatus { + opts.Copy.CHOpVersion = true + opts.Copy.CHOpCommit = true + opts.Copy.CHOpDate = true + opts.Copy.CHOpIP = true + opts.Copy.ClustersCount = true + opts.Copy.ShardsCount = true + opts.Copy.ReplicasCount = true + opts.Copy.HostsCount = true + opts.Copy.Status = true + opts.Copy.TaskID = true + opts.Copy.TaskIDsStarted = true + opts.Copy.TaskIDsCompleted = true + opts.Copy.Action = true + opts.Merge.Actions = true + opts.Copy.Error = true + opts.Copy.Errors = true + opts.Copy.HostsUpdatedCount = true + opts.Copy.HostsAddedCount = true + opts.Copy.HostsUnchangedCount = true + opts.Copy.HostsCompletedCount = true + opts.Copy.HostsDeletedCount = true + opts.Copy.HostsDeleteCount = true + opts.Copy.Pods = true + opts.Copy.PodIPs = true + opts.Copy.FQDNs = true + opts.Copy.Endpoint = true + opts.Copy.NormalizedCR = true + opts.Copy.NormalizedCRCompleted = true + opts.Copy.ActionPlan = true + opts.Copy.UsedTemplates = true + } + + return opts +} + +// CopyFrom copies the state of a given Status f into the receiver Status of the call. +func (s *Status) CopyFrom(f *Status, opts types.CopyStatusOptions) { + doWithWriteLock(s, func(s *Status) { + doWithReadLock(f, func(from *Status) { + if s == nil || from == nil { + return + } + + opts = prepareOptions(opts) + + // Copy fields + if opts.Copy.CHOpVersion { + s.CHOpVersion = from.CHOpVersion + } + if opts.Copy.CHOpCommit { + s.CHOpCommit = from.CHOpCommit + } + if opts.Copy.CHOpDate { + s.CHOpDate = from.CHOpDate + } + if opts.Copy.CHOpIP { + s.CHOpIP = from.CHOpIP + } + if opts.Copy.ClustersCount { + s.ClustersCount = from.ClustersCount + } + if opts.Copy.ShardsCount { + s.ShardsCount = from.ShardsCount + } + if opts.Copy.ReplicasCount { + s.ReplicasCount = from.ReplicasCount + } + if opts.Copy.HostsCount { + s.HostsCount = from.HostsCount + } + if opts.Copy.Status { + s.Status = from.Status + } + if opts.Copy.TaskID { + s.TaskID = from.TaskID + } + if opts.Copy.TaskIDsStarted { + s.TaskIDsStarted = from.TaskIDsStarted + } + if opts.Copy.TaskIDsCompleted { + s.TaskIDsCompleted = from.TaskIDsCompleted + } + if opts.Copy.Action { + s.Action = from.Action + } + if opts.Merge.Actions { + mergeActionsNoSync(s, from) + } + if opts.Copy.Error { + s.Error = from.Error + } + if opts.Copy.Errors { + s.Errors = from.Errors + } + if opts.Merge.Errors { + s.Errors = util.MergeStringArrays(s.Errors, from.Errors) + sort.Sort(sort.Reverse(sort.StringSlice(s.Errors))) + } + if opts.Copy.HostsUpdatedCount { + s.HostsUpdatedCount = from.HostsUpdatedCount + } + if opts.Copy.HostsAddedCount { + s.HostsAddedCount = from.HostsAddedCount + } + if opts.Copy.HostsUnchangedCount { + s.HostsUnchangedCount = from.HostsUnchangedCount + } + if opts.Copy.HostsCompletedCount { + s.HostsCompletedCount = from.HostsCompletedCount + } + if opts.Copy.HostsDeletedCount { + s.HostsDeletedCount = from.HostsDeletedCount + } + if opts.Copy.HostsDeleteCount { + s.HostsDeleteCount = from.HostsDeleteCount + } + if opts.Copy.Pods { + s.Pods = from.Pods + } + if opts.Copy.PodIPs { + s.PodIPs = from.PodIPs + } + if opts.Copy.FQDNs { + s.FQDNs = from.FQDNs + } + if opts.Copy.Endpoint { + s.Endpoint = from.Endpoint + s.Endpoints = from.Endpoints + } + if opts.Copy.NormalizedCR { + s.NormalizedCR = from.NormalizedCR + } + if opts.Copy.NormalizedCRCompleted { + s.NormalizedCRCompleted = from.NormalizedCRCompleted + } + if opts.Copy.ActionPlan { + s.ActionPlan = from.ActionPlan + } + if opts.Copy.HostsWithTablesCreated { + s.HostsWithTablesCreated = nil + if len(from.HostsWithTablesCreated) > 0 { + s.HostsWithTablesCreated = append(s.HostsWithTablesCreated, from.HostsWithTablesCreated...) + } + s.HostsWithReplicaCaughtUp = nil + if len(from.HostsWithReplicaCaughtUp) > 0 { + s.HostsWithReplicaCaughtUp = append(s.HostsWithReplicaCaughtUp, from.HostsWithReplicaCaughtUp...) + } + } + if opts.Copy.UsedTemplates { + if len(from.UsedTemplates) > len(s.UsedTemplates) { + s.UsedTemplates = nil + s.UsedTemplates = append(s.UsedTemplates, from.UsedTemplates...) + } + } + }) + }) +} + +// ClearNormalizedCR clears normalized CR in status +func (s *Status) ClearNormalizedCR() { + doWithWriteLock(s, func(s *Status) { + s.NormalizedCR = nil + }) +} + +// SetNormalizedCompletedFromCurrentNormalized sets completed CR from current CR +func (s *Status) SetNormalizedCompletedFromCurrentNormalized() { + doWithWriteLock(s, func(s *Status) { + s.NormalizedCRCompleted = s.NormalizedCR + }) +} + +// GetCHOpVersion gets operator version +func (s *Status) GetCHOpVersion() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.CHOpVersion + }) +} + +// GetCHOpCommit gets operator build commit +func (s *Status) GetCHOpCommit() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.CHOpCommit + }) +} + +// GetCHOpDate gets operator build date +func (s *Status) GetCHOpDate() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.CHOpDate + }) +} + +// GetCHOpIP gets operator pod's IP +func (s *Status) GetCHOpIP() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.CHOpIP + }) +} + +// GetClustersCount gets clusters count +func (s *Status) GetClustersCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.ClustersCount + }) +} + +// GetShardsCount gets shards count +func (s *Status) GetShardsCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.ShardsCount + }) +} + +// GetReplicasCount gets replicas count +func (s *Status) GetReplicasCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.ReplicasCount + }) +} + +// GetHostsCount gets hosts count +func (s *Status) GetHostsCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsCount + }) +} + +// GetStatus gets status +func (s *Status) GetStatus() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.Status + }) +} + +// GetTaskID gets task ipd +func (s *Status) GetTaskID() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.TaskID + }) +} + +// GetTaskIDsStarted gets started task id +func (s *Status) GetTaskIDsStarted() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.TaskIDsStarted + }) +} + +// GetTaskIDsCompleted gets completed task id +func (s *Status) GetTaskIDsCompleted() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.TaskIDsCompleted + }) +} + +// GetAction gets last action +func (s *Status) GetAction() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.Action + }) +} + +// GetActions gets all actions +func (s *Status) GetActions() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.Actions + }) +} + +// GetError gets last error +func (s *Status) GetError() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.Error + }) +} + +// GetErrors gets all errors +func (s *Status) GetErrors() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.Errors + }) +} + +// GetHostsUpdatedCount gets updated hosts counter +func (s *Status) GetHostsUpdatedCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsUpdatedCount + }) +} + +// GetHostsAddedCount gets added hosts counter +func (s *Status) GetHostsAddedCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsAddedCount + }) +} + +// GetHostsUnchangedCount gets unchanged hosts counter +func (s *Status) GetHostsUnchangedCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsUnchangedCount + }) +} + +// GetHostsFailedCount gets failed hosts counter +func (s *Status) GetHostsFailedCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsFailedCount + }) +} + +// GetHostsCompletedCount gets completed hosts counter +func (s *Status) GetHostsCompletedCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsCompletedCount + }) +} + +// GetHostsDeletedCount gets deleted hosts counter +func (s *Status) GetHostsDeletedCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsDeletedCount + }) +} + +// GetHostsDeleteCount gets hosts to be deleted counter +func (s *Status) GetHostsDeleteCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsDeleteCount + }) +} + +// GetPods gets list of pods +func (s *Status) GetPods() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.Pods + }) +} + +// GetPodIPs gets list of pod ips +func (s *Status) GetPodIPs() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.PodIPs + }) +} + +// GetFQDNs gets list of all FQDNs of hosts +func (s *Status) GetFQDNs() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.FQDNs + }) +} + +// GetEndpoint gets API endpoint +func (s *Status) GetEndpoint() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.Endpoint + }) +} + +// GetNormalizedCR gets target CR +func (s *Status) GetNormalizedCR() *ClickHouseKeeperInstallation { + return getCRWithReadLock(s, func(s *Status) *ClickHouseKeeperInstallation { + return s.NormalizedCR + }) +} + +// GetNormalizedCRCompleted gets completed CR +func (s *Status) GetNormalizedCRCompleted() *ClickHouseKeeperInstallation { + return getCRWithReadLock(s, func(s *Status) *ClickHouseKeeperInstallation { + return s.NormalizedCRCompleted + }) +} + +// GetHostsWithTablesCreated gets hosts with created tables +func (s *Status) GetHostsWithTablesCreated() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return util.NormalizeFQDNs(s.HostsWithTablesCreated) + }) +} + +// GetHostsWithReplicaCaughtUp gets hosts with replica caught-up +func (s *Status) GetHostsWithReplicaCaughtUp() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return util.NormalizeFQDNs(s.HostsWithReplicaCaughtUp) + }) +} + +// Begin helpers + +func doWithWriteLock(s *Status, f func(*Status)) { + if s == nil { + return + } + + s.mu.Lock() + defer s.mu.Unlock() + f(s) +} + +func doWithReadLock(s *Status, f func(*Status)) { + if s == nil { + return + } + + s.mu.RLock() + defer s.mu.RUnlock() + f(s) +} + +func getIntWithReadLock(s *Status, f func(*Status) int) int { + var zeroVal int + if s == nil { + return zeroVal + } + + s.mu.RLock() + defer s.mu.RUnlock() + return f(s) +} + +func getStringWithReadLock(s *Status, f func(*Status) string) string { + var zeroVal string + if s == nil { + return zeroVal + } + + s.mu.RLock() + defer s.mu.RUnlock() + return f(s) +} + +func getCRWithReadLock(s *Status, f func(*Status) *ClickHouseKeeperInstallation) *ClickHouseKeeperInstallation { + var zeroVal *ClickHouseKeeperInstallation + if s == nil { + return zeroVal + } + + s.mu.RLock() + defer s.mu.RUnlock() + return f(s) +} + +func getStringArrWithReadLock(s *Status, f func(*Status) []string) []string { + emptyArr := make([]string, 0, 0) + if s == nil { + return emptyArr + } + + s.mu.RLock() + defer s.mu.RUnlock() + return f(s) +} + +// mergeActionsNoSync merges the actions of from into those of s (without synchronization, because synchronized +// functions call into this). +func mergeActionsNoSync(s *Status, from *Status) { + s.Actions = util.MergeStringArrays(s.Actions, from.Actions) + sort.Sort(sort.Reverse(sort.StringSlice(s.Actions))) + trimActionsNoSync(s) +} + +// trimActionsNoSync trims actions (without synchronization, because synchronized functions call into this). +func trimActionsNoSync(s *Status) { + if len(s.Actions) > maxActions { + s.Actions = s.Actions[:maxActions] + } +} + +// pushTaskIDStartedNoSync pushes task id into status +func pushTaskIDStartedNoSync(s *Status) { + s.TaskIDsStarted = append([]string{s.TaskID}, s.TaskIDsStarted...) + if len(s.TaskIDsStarted) > maxTaskIDs { + s.TaskIDsStarted = s.TaskIDsStarted[:maxTaskIDs] + } +} + +// pushTaskIDCompletedNoSync pushes task id into status w/o sync +func pushTaskIDCompletedNoSync(s *Status) { + s.TaskIDsCompleted = append([]string{s.TaskID}, s.TaskIDsCompleted...) + if len(s.TaskIDsCompleted) > maxTaskIDs { + s.TaskIDsCompleted = s.TaskIDsCompleted[:maxTaskIDs] + } +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/types.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/types.go new file mode 100644 index 00000000..38522eac --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/types.go @@ -0,0 +1,74 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "sync" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + apiChi "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/swversion" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClickHouseKeeperInstallation defines a ClickHouse Keeper ChkCluster +type ClickHouseKeeperInstallation struct { + meta.TypeMeta `json:",inline" yaml:",inline"` + meta.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"` + + Spec ChkSpec `json:"spec" yaml:"spec"` + Status *Status `json:"status,omitempty" yaml:"status,omitempty"` + + runtime *ClickHouseKeeperInstallationRuntime `json:"-" yaml:"-"` + statusCreatorMutex sync.Mutex `json:"-" yaml:"-"` + runtimeCreatorMutex sync.Mutex `json:"-" yaml:"-"` +} + +type ClickHouseKeeperInstallationRuntime struct { + attributes *apiChi.ComparableAttributes `json:"-" yaml:"-"` + commonConfigMutex sync.Mutex `json:"-" yaml:"-"` + MinVersion *swversion.SoftWareVersion `json:"-" yaml:"-"` + MaxVersion *swversion.SoftWareVersion `json:"-" yaml:"-"` + ActionPlan apiChi.IActionPlan `json:"-" yaml:"-"` +} + +func newClickHouseKeeperInstallationRuntime() *ClickHouseKeeperInstallationRuntime { + return &ClickHouseKeeperInstallationRuntime{ + attributes: &apiChi.ComparableAttributes{}, + } +} + +func (runtime *ClickHouseKeeperInstallationRuntime) GetAttributes() *apiChi.ComparableAttributes { + return runtime.attributes +} + +func (runtime *ClickHouseKeeperInstallationRuntime) LockCommonConfig() { + runtime.commonConfigMutex.Lock() +} + +func (runtime *ClickHouseKeeperInstallationRuntime) UnlockCommonConfig() { + runtime.commonConfigMutex.Unlock() +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClickHouseKeeperList defines a list of ClickHouseKeeper resources +type ClickHouseKeeperInstallationList struct { + meta.TypeMeta `json:",inline" yaml:",inline"` + meta.ListMeta `json:"metadata" yaml:"metadata"` + Items []ClickHouseKeeperInstallation `json:"items" yaml:"items"` +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/zz_generated.deepcopy.go b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/zz_generated.deepcopy.go new file mode 100644 index 00000000..7d2b04c5 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1/zz_generated.deepcopy.go @@ -0,0 +1,682 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1 + +import ( + clickhousealtinitycomv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + types "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChkClusterAddress) DeepCopyInto(out *ChkClusterAddress) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChkClusterAddress. +func (in *ChkClusterAddress) DeepCopy() *ChkClusterAddress { + if in == nil { + return nil + } + out := new(ChkClusterAddress) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChkClusterLayout) DeepCopyInto(out *ChkClusterLayout) { + *out = *in + if in.Shards != nil { + in, out := &in.Shards, &out.Shards + *out = make([]*ChkShard, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(ChkShard) + (*in).DeepCopyInto(*out) + } + } + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = make([]*ChkReplica, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(ChkReplica) + (*in).DeepCopyInto(*out) + } + } + } + if in.HostsField != nil { + in, out := &in.HostsField, &out.HostsField + *out = new(clickhousealtinitycomv1.HostsField) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChkClusterLayout. +func (in *ChkClusterLayout) DeepCopy() *ChkClusterLayout { + if in == nil { + return nil + } + out := new(ChkClusterLayout) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChkClusterRuntime) DeepCopyInto(out *ChkClusterRuntime) { + *out = *in + out.Address = in.Address + if in.CHK != nil { + in, out := &in.CHK, &out.CHK + *out = new(ClickHouseKeeperInstallation) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChkClusterRuntime. +func (in *ChkClusterRuntime) DeepCopy() *ChkClusterRuntime { + if in == nil { + return nil + } + out := new(ChkClusterRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChkReplica) DeepCopyInto(out *ChkReplica) { + *out = *in + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = new(clickhousealtinitycomv1.Settings) + (*in).DeepCopyInto(*out) + } + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = new(clickhousealtinitycomv1.Settings) + (*in).DeepCopyInto(*out) + } + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = new(clickhousealtinitycomv1.TemplatesList) + (*in).DeepCopyInto(*out) + } + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]*clickhousealtinitycomv1.Host, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(clickhousealtinitycomv1.Host) + (*in).DeepCopyInto(*out) + } + } + } + in.Runtime.DeepCopyInto(&out.Runtime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChkReplica. +func (in *ChkReplica) DeepCopy() *ChkReplica { + if in == nil { + return nil + } + out := new(ChkReplica) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChkReplicaAddress) DeepCopyInto(out *ChkReplicaAddress) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChkReplicaAddress. +func (in *ChkReplicaAddress) DeepCopy() *ChkReplicaAddress { + if in == nil { + return nil + } + out := new(ChkReplicaAddress) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChkReplicaRuntime) DeepCopyInto(out *ChkReplicaRuntime) { + *out = *in + out.Address = in.Address + if in.CHK != nil { + in, out := &in.CHK, &out.CHK + *out = new(ClickHouseKeeperInstallation) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChkReplicaRuntime. +func (in *ChkReplicaRuntime) DeepCopy() *ChkReplicaRuntime { + if in == nil { + return nil + } + out := new(ChkReplicaRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChkShard) DeepCopyInto(out *ChkShard) { + *out = *in + if in.Weight != nil { + in, out := &in.Weight, &out.Weight + *out = new(int) + **out = **in + } + if in.InternalReplication != nil { + in, out := &in.InternalReplication, &out.InternalReplication + *out = new(types.StringBool) + **out = **in + } + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = new(clickhousealtinitycomv1.Settings) + (*in).DeepCopyInto(*out) + } + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = new(clickhousealtinitycomv1.Settings) + (*in).DeepCopyInto(*out) + } + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = new(clickhousealtinitycomv1.TemplatesList) + (*in).DeepCopyInto(*out) + } + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]*clickhousealtinitycomv1.Host, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(clickhousealtinitycomv1.Host) + (*in).DeepCopyInto(*out) + } + } + } + in.Runtime.DeepCopyInto(&out.Runtime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChkShard. +func (in *ChkShard) DeepCopy() *ChkShard { + if in == nil { + return nil + } + out := new(ChkShard) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChkShardAddress) DeepCopyInto(out *ChkShardAddress) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChkShardAddress. +func (in *ChkShardAddress) DeepCopy() *ChkShardAddress { + if in == nil { + return nil + } + out := new(ChkShardAddress) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChkShardRuntime) DeepCopyInto(out *ChkShardRuntime) { + *out = *in + out.Address = in.Address + if in.CHK != nil { + in, out := &in.CHK, &out.CHK + *out = new(ClickHouseKeeperInstallation) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChkShardRuntime. +func (in *ChkShardRuntime) DeepCopy() *ChkShardRuntime { + if in == nil { + return nil + } + out := new(ChkShardRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChkSpec) DeepCopyInto(out *ChkSpec) { + *out = *in + if in.TaskID != nil { + in, out := &in.TaskID, &out.TaskID + *out = new(types.Id) + **out = **in + } + if in.Stop != nil { + in, out := &in.Stop, &out.Stop + *out = new(types.StringBool) + **out = **in + } + if in.NamespaceDomainPattern != nil { + in, out := &in.NamespaceDomainPattern, &out.NamespaceDomainPattern + *out = new(types.String) + **out = **in + } + if in.Suspend != nil { + in, out := &in.Suspend, &out.Suspend + *out = new(types.StringBool) + **out = **in + } + if in.Reconciling != nil { + in, out := &in.Reconciling, &out.Reconciling + *out = new(clickhousealtinitycomv1.ChiReconcile) + (*in).DeepCopyInto(*out) + } + if in.Reconcile != nil { + in, out := &in.Reconcile, &out.Reconcile + *out = new(clickhousealtinitycomv1.ChiReconcile) + (*in).DeepCopyInto(*out) + } + if in.Defaults != nil { + in, out := &in.Defaults, &out.Defaults + *out = new(clickhousealtinitycomv1.Defaults) + (*in).DeepCopyInto(*out) + } + if in.Configuration != nil { + in, out := &in.Configuration, &out.Configuration + *out = new(Configuration) + (*in).DeepCopyInto(*out) + } + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = new(clickhousealtinitycomv1.Templates) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChkSpec. +func (in *ChkSpec) DeepCopy() *ChkSpec { + if in == nil { + return nil + } + out := new(ChkSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseKeeperInstallation) DeepCopyInto(out *ClickHouseKeeperInstallation) { + // *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = new(Status) + (*in).DeepCopyInto(*out) + } + if in.runtime != nil { + in, out := &in.runtime, &out.runtime + *out = new(ClickHouseKeeperInstallationRuntime) + (*in).DeepCopyInto(*out) + } + // out.statusCreatorMutex = in.statusCreatorMutex + // out.runtimeCreatorMutex = in.runtimeCreatorMutex + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseKeeperInstallation. +func (in *ClickHouseKeeperInstallation) DeepCopy() *ClickHouseKeeperInstallation { + if in == nil { + return nil + } + out := new(ClickHouseKeeperInstallation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClickHouseKeeperInstallation) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseKeeperInstallationList) DeepCopyInto(out *ClickHouseKeeperInstallationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClickHouseKeeperInstallation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseKeeperInstallationList. +func (in *ClickHouseKeeperInstallationList) DeepCopy() *ClickHouseKeeperInstallationList { + if in == nil { + return nil + } + out := new(ClickHouseKeeperInstallationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClickHouseKeeperInstallationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseKeeperInstallationRuntime) DeepCopyInto(out *ClickHouseKeeperInstallationRuntime) { + // *out = *in + if in.attributes != nil { + in, out := &in.attributes, &out.attributes + *out = new(clickhousealtinitycomv1.ComparableAttributes) + (*in).DeepCopyInto(*out) + } + // out.commonConfigMutex = in.commonConfigMutex + if in.MinVersion != nil { + in, out := &in.MinVersion, &out.MinVersion + *out = (*in).DeepCopy() + } + if in.MaxVersion != nil { + in, out := &in.MaxVersion, &out.MaxVersion + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseKeeperInstallationRuntime. +func (in *ClickHouseKeeperInstallationRuntime) DeepCopy() *ClickHouseKeeperInstallationRuntime { + if in == nil { + return nil + } + out := new(ClickHouseKeeperInstallationRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Cluster) DeepCopyInto(out *Cluster) { + *out = *in + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = new(clickhousealtinitycomv1.Settings) + (*in).DeepCopyInto(*out) + } + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = new(clickhousealtinitycomv1.Settings) + (*in).DeepCopyInto(*out) + } + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = new(clickhousealtinitycomv1.TemplatesList) + (*in).DeepCopyInto(*out) + } + if in.Layout != nil { + in, out := &in.Layout, &out.Layout + *out = new(ChkClusterLayout) + (*in).DeepCopyInto(*out) + } + if in.PDBManaged != nil { + in, out := &in.PDBManaged, &out.PDBManaged + *out = new(types.StringBool) + **out = **in + } + if in.PDBMaxUnavailable != nil { + in, out := &in.PDBMaxUnavailable, &out.PDBMaxUnavailable + *out = new(types.Int32) + **out = **in + } + if in.Reconcile != nil { + in, out := &in.Reconcile, &out.Reconcile + *out = new(clickhousealtinitycomv1.ClusterReconcile) + (*in).DeepCopyInto(*out) + } + in.Runtime.DeepCopyInto(&out.Runtime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cluster. +func (in *Cluster) DeepCopy() *Cluster { + if in == nil { + return nil + } + out := new(Cluster) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Configuration) DeepCopyInto(out *Configuration) { + *out = *in + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = new(clickhousealtinitycomv1.Settings) + (*in).DeepCopyInto(*out) + } + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = new(clickhousealtinitycomv1.Settings) + (*in).DeepCopyInto(*out) + } + if in.Clusters != nil { + in, out := &in.Clusters, &out.Clusters + *out = make([]*Cluster, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Cluster) + (*in).DeepCopyInto(*out) + } + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration. +func (in *Configuration) DeepCopy() *Configuration { + if in == nil { + return nil + } + out := new(Configuration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FillStatusParams) DeepCopyInto(out *FillStatusParams) { + *out = *in + if in.Pods != nil { + in, out := &in.Pods, &out.Pods + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.FQDNs != nil { + in, out := &in.FQDNs, &out.FQDNs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Endpoints != nil { + in, out := &in.Endpoints, &out.Endpoints + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.NormalizedCR != nil { + in, out := &in.NormalizedCR, &out.NormalizedCR + *out = new(ClickHouseKeeperInstallation) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FillStatusParams. +func (in *FillStatusParams) DeepCopy() *FillStatusParams { + if in == nil { + return nil + } + out := new(FillStatusParams) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Status) DeepCopyInto(out *Status) { + // *out = *in + out.CHOpVersion = in.CHOpVersion + out.CHOpCommit = in.CHOpCommit + out.CHOpDate = in.CHOpDate + out.CHOpIP = in.CHOpIP + out.ClustersCount = in.ClustersCount + out.ShardsCount = in.ShardsCount + out.ReplicasCount = in.ReplicasCount + out.HostsCount = in.HostsCount + out.Status = in.Status + out.TaskID = in.TaskID + out.Action = in.Action + out.Error = in.Error + out.HostsUpdatedCount = in.HostsUpdatedCount + out.HostsAddedCount = in.HostsAddedCount + out.HostsUnchangedCount = in.HostsUnchangedCount + out.HostsFailedCount = in.HostsFailedCount + out.HostsCompletedCount = in.HostsCompletedCount + out.HostsDeletedCount = in.HostsDeletedCount + out.HostsDeleteCount = in.HostsDeleteCount + out.Endpoint = in.Endpoint + if in.TaskIDsStarted != nil { + in, out := &in.TaskIDsStarted, &out.TaskIDsStarted + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TaskIDsCompleted != nil { + in, out := &in.TaskIDsCompleted, &out.TaskIDsCompleted + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Actions != nil { + in, out := &in.Actions, &out.Actions + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Errors != nil { + in, out := &in.Errors, &out.Errors + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Pods != nil { + in, out := &in.Pods, &out.Pods + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PodIPs != nil { + in, out := &in.PodIPs, &out.PodIPs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.FQDNs != nil { + in, out := &in.FQDNs, &out.FQDNs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Endpoints != nil { + in, out := &in.Endpoints, &out.Endpoints + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.NormalizedCR != nil { + in, out := &in.NormalizedCR, &out.NormalizedCR + *out = new(ClickHouseKeeperInstallation) + (*in).DeepCopyInto(*out) + } + if in.NormalizedCRCompleted != nil { + in, out := &in.NormalizedCRCompleted, &out.NormalizedCRCompleted + *out = new(ClickHouseKeeperInstallation) + (*in).DeepCopyInto(*out) + } + if in.ActionPlan != nil { + in, out := &in.ActionPlan, &out.ActionPlan + *out = new(clickhousealtinitycomv1.ActionPlan) + (*in).DeepCopyInto(*out) + } + if in.HostsWithTablesCreated != nil { + in, out := &in.HostsWithTablesCreated, &out.HostsWithTablesCreated + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.HostsWithReplicaCaughtUp != nil { + in, out := &in.HostsWithReplicaCaughtUp, &out.HostsWithReplicaCaughtUp + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.UsedTemplates != nil { + in, out := &in.UsedTemplates, &out.UsedTemplates + *out = make([]*clickhousealtinitycomv1.TemplateRef, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(clickhousealtinitycomv1.TemplateRef) + **out = **in + } + } + } + // out.mu = in.mu + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. +func (in *Status) DeepCopy() *Status { + if in == nil { + return nil + } + out := new(Status) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/api_group.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/api_group.go new file mode 100644 index 00000000..838972d0 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/api_group.go @@ -0,0 +1,20 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package clickhouse_altinity_com + +const ( + // APIGroupName is the group name of the ClickHouse Operator API. + APIGroupName = "clickhouse.altinity.com" +) diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/action_plan.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/action_plan.go new file mode 100644 index 00000000..558ae7c8 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/action_plan.go @@ -0,0 +1,425 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "fmt" + + "gopkg.in/d4l3k/messagediff.v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" +) + +// ActionPlan is an action plan with list of differences between two CHIs +type ActionPlan struct { + old ICustomResource + new ICustomResource + + specDiff *messagediff.Diff + specDiffReverse *messagediff.Diff + specEqual bool + + labelsDiff *messagediff.Diff + labelsEqual bool + + deletionTimestampDiff *messagediff.Diff + deletionTimestampEqual bool + + finalizersDiff *messagediff.Diff + finalizersEqual bool + + attributesDiff *messagediff.Diff + attributesEqual bool + + skipTaskID bool +} + +func NewActionPlan() *ActionPlan { + return &ActionPlan{} +} + +// MakeActionPlan makes new ActionPlan out of two CHIs +func MakeActionPlan(old, new ICustomResource) IActionPlan { + ap := &ActionPlan{ + old: old, + new: new, + } + + if (old != nil) && (new != nil) { + ap.specDiff, ap.specEqual = messagediff.DeepDiff(ap.old.GetSpecA(), ap.new.GetSpecA()) + ap.specDiffReverse, _ = messagediff.DeepDiff(ap.new.GetSpecA(), ap.old.GetSpecA()) + ap.labelsDiff, ap.labelsEqual = messagediff.DeepDiff(ap.old.GetLabels(), ap.new.GetLabels()) + ap.deletionTimestampDiff, _ = messagediff.DeepDiff(ap.old.GetDeletionTimestamp(), ap.new.GetDeletionTimestamp()) + ap.deletionTimestampEqual = ap.timestampEqual(ap.old.GetDeletionTimestamp(), ap.new.GetDeletionTimestamp()) + ap.finalizersDiff, ap.finalizersEqual = messagediff.DeepDiff(ap.old.GetFinalizers(), ap.new.GetFinalizers()) + ap.attributesDiff, ap.attributesEqual = messagediff.DeepDiff(ap.old.GetRuntime().GetAttributes(), ap.new.GetRuntime().GetAttributes()) + } else if old == nil { + ap.specDiff, ap.specEqual = messagediff.DeepDiff(nil, ap.new.GetSpecA()) + ap.labelsDiff, ap.labelsEqual = messagediff.DeepDiff(nil, ap.new.GetLabels()) + ap.deletionTimestampEqual = ap.timestampEqual(nil, ap.new.GetDeletionTimestamp()) + ap.deletionTimestampDiff, _ = messagediff.DeepDiff(nil, ap.new.GetDeletionTimestamp()) + ap.finalizersDiff, ap.finalizersEqual = messagediff.DeepDiff(nil, ap.new.GetFinalizers()) + ap.attributesDiff, ap.attributesEqual = messagediff.DeepDiff(nil, ap.new.GetRuntime().GetAttributes()) + } else if new == nil { + ap.specDiff, ap.specEqual = messagediff.DeepDiff(ap.old.GetSpecA(), nil) + ap.labelsDiff, ap.labelsEqual = messagediff.DeepDiff(ap.old.GetLabels(), nil) + ap.deletionTimestampEqual = ap.timestampEqual(ap.old.GetDeletionTimestamp(), nil) + ap.deletionTimestampDiff, _ = messagediff.DeepDiff(ap.old.GetDeletionTimestamp(), nil) + ap.finalizersDiff, ap.finalizersEqual = messagediff.DeepDiff(ap.old.GetFinalizers(), nil) + ap.attributesDiff, ap.attributesEqual = messagediff.DeepDiff(ap.old.GetRuntime().GetAttributes(), nil) + } else { + // Both are nil + ap.specDiff = nil + ap.specEqual = true + + ap.labelsDiff = nil + ap.labelsEqual = true + + ap.deletionTimestampDiff = nil + ap.deletionTimestampEqual = true + + ap.finalizersDiff = nil + ap.finalizersEqual = true + + ap.attributesDiff = nil + ap.attributesEqual = true + } + + if (new != nil) && new.GetSpec().GetTaskID().IsAutoId() { + ap.skipTaskID = true + } + + ap.excludePaths() + + return ap +} + +func (ap *ActionPlan) timestampEqual(old, new *meta.Time) bool { + switch { + case (old == nil) && (new == nil): + // Both are useless - consider equal + return true + case (old == nil) && (new != nil): + // Timestamp is assigned - unequal + return false + case (old != nil) && (new == nil): + // Timestamp unassigned - unequal + return false + case (old != nil) && (new != nil): + // Both have value - need to compare + return old.Equal(new) + default: + // WTF? + return false + } +} + +// excludePaths - sanitize diff - do not pay attention to changes in some paths, such as ObjectMeta.ResourceVersion +func (ap *ActionPlan) excludePaths() { + // Sanity check + if ap.specDiff == nil { + return + } + + // List of paths to be excluded + var excludePaths []*messagediff.Path + + // Walk over all .diff.Modified paths and find all paths that are to be excluded from modified + for path := range ap.specDiff.Modified { + if ap.isExcludedPath(path) { + // This path should be excluded from Modified + excludePaths = append(excludePaths, path) + } + } + + // Exclude paths from diff.Modified according to the list of paths to be excluded + for _, path := range excludePaths { + delete(ap.specDiff.Modified, path) + } +} + +// isExcludedPath checks whether path is excluded +func (ap *ActionPlan) isExcludedPath(path *messagediff.Path) bool { + // Walk over all path's segments and check whether any segment is excluded + for i := range *path { + pathNodeCurr := (*path)[i] + pathNodePrev := (*path)[i] + if i > 0 { + // We have prev node + pathNodePrev = (*path)[i-1] + } + + if ap.isExcludedPathSegment(pathNodePrev.String(), pathNodeCurr.String()) { + // Path has segment which specifies to exclude this path + return true + } + } + + return false +} + +// isExcludedPathSegment checks whether path segment is excluded +func (ap *ActionPlan) isExcludedPathSegment(prev, cur string) bool { + if ((prev == "ObjectMeta") && (cur == ".ResourceVersion")) || + ((prev == ".ObjectMeta") && (cur == ".ResourceVersion")) { + return true + } + + if ((prev == "Status") && (cur == "Status")) || + ((prev == ".Status") && (cur == ".Status")) { + return true + } + + if ((prev == "Runtime") && (cur == "Version")) || + ((prev == ".Runtime") && (cur == ".Version")) { + return true + } + + if (((prev == "TaskID") && (cur == ".TaskID")) || + ((prev == ".TaskID") && (cur == ".TaskID"))) && ap.skipTaskID { + return true + } + + return false +} + +// HasActionsToDo checks whether there are any actions to do - meaning changes between states to reconcile +func (ap *ActionPlan) HasActionsToDo() bool { + if ap.specEqual && ap.labelsEqual && ap.deletionTimestampEqual && ap.finalizersEqual && ap.attributesEqual { + // All is equal - no actions to do + return false + } + + // Something is not equal + + if ap.specDiff != nil { + if len(ap.specDiff.Added)+len(ap.specDiff.Removed)+len(ap.specDiff.Modified) > 0 { + // Spec section has some modifications + return true + } + } + + if ap.labelsDiff != nil { + if len(ap.labelsDiff.Added)+len(ap.labelsDiff.Removed)+len(ap.labelsDiff.Modified) > 0 { + // Labels section has some modifications + return true + } + } + + return !ap.deletionTimestampEqual || !ap.finalizersEqual || !ap.attributesEqual +} + +func (ap *ActionPlan) Log(tag string) string { + return fmt.Sprintf( + "\nActionPlan start %s ---------------------------------------------:\n%s\nActionPlan end %s ---------------------------------------------", + tag, + ap, + tag, + ) +} + +// String stringifies ActionPlan +func (ap *ActionPlan) String() string { + if !ap.HasActionsToDo() { + return "" + } + + str := "" + + if len(ap.specDiff.Added) > 0 { + // Something added + str += util.MessageDiffItemString("added spec items", "none", "", ap.specDiff.Added) + } + + if len(ap.specDiff.Removed) > 0 { + // Something removed + str += util.MessageDiffItemString("removed spec items", "none", "", ap.specDiff.Removed) + } + + if len(ap.specDiff.Modified) > 0 { + // Something modified + str += util.MessageDiffItemString("modified spec items", "none", "", ap.specDiff.Modified) + } + + if len(ap.specDiffReverse.Modified) > 0 { + // Something modified + str += util.MessageDiffItemString("prev spec items", "none", "", ap.specDiffReverse.Modified) + } + + if len(ap.labelsDiff.Added) > 0 { + // Something added + str += "added labels\n" + } + + if len(ap.labelsDiff.Removed) > 0 { + // Something removed + str += "removed labels\n" + } + + if len(ap.labelsDiff.Modified) > 0 { + // Something modified + str += "modified labels\n" + } + + if !ap.deletionTimestampEqual { + str += "modified deletion timestamp:\n" + str += util.MessageDiffItemString("modified deletion timestamp", "none", ".metadata.deletionTimestamp", ap.deletionTimestampDiff.Modified) + } + + if !ap.finalizersEqual { + str += "modified finalizer:\n" + str += util.MessageDiffItemString("modified finalizers", "none", ".metadata.finalizers", ap.finalizersDiff.Modified) + } + + return str +} + +// GetRemovedHostsNum - how many hosts would be removed +func (ap *ActionPlan) GetRemovedHostsNum() int { + var count int + ap.WalkRemoved( + func(cluster ICluster) { + count += cluster.HostsCount() + }, + func(shard IShard) { + count += shard.HostsCount() + }, + func(host *Host) { + count++ + }, + ) + return count +} + +// WalkRemoved walk removed cluster items +func (ap *ActionPlan) WalkRemoved( + clusterFunc func(cluster ICluster), + shardFunc func(shard IShard), + hostFunc func(host *Host), +) { + if ap == nil { + return + } + // TODO refactor to map[string]object handling, instead of slice + for path := range ap.specDiff.Removed { + switch ap.specDiff.Removed[path].(type) { + //case ChiCluster: + // cluster := ap.specDiff.Removed[path].(api.ChiCluster) + // clusterFunc(&cluster) + //case api.ChiShard: + // shard := ap.specDiff.Removed[path].(api.ChiShard) + // shardFunc(&shard) + //case api.Host: + // host := ap.specDiff.Removed[path].(api.Host) + // hostFunc(&host) + //case *api.ChiCluster: + // cluster := ap.specDiff.Removed[path].(*api.ChiCluster) + // clusterFunc(cluster) + case ICluster: + cluster := ap.specDiff.Removed[path].(ICluster) + clusterFunc(cluster) + //case *api.ChiShard: + // shard := ap.specDiff.Removed[path].(*api.ChiShard) + // shardFunc(shard) + case IShard: + shard := ap.specDiff.Removed[path].(IShard) + shardFunc(shard) + case *Host: + host := ap.specDiff.Removed[path].(*Host) + hostFunc(host) + } + } +} + +// WalkAdded walk added cluster items +func (ap *ActionPlan) WalkAdded( + clusterFunc func(cluster ICluster), + shardFunc func(shard IShard), + hostFunc func(host *Host), +) { + if ap == nil { + return + } + // TODO refactor to map[string]object handling, instead of slice + for path := range ap.specDiff.Added { + switch ap.specDiff.Added[path].(type) { + //case api.ChiCluster: + // cluster := ap.specDiff.Added[path].(api.ChiCluster) + // clusterFunc(&cluster) + //case api.ChiShard: + // shard := ap.specDiff.Added[path].(api.ChiShard) + // shardFunc(&shard) + //case api.Host: + // host := ap.specDiff.Added[path].(api.Host) + // hostFunc(&host) + //case *api.ChiCluster: + // cluster := ap.specDiff.Added[path].(*api.ChiCluster) + // clusterFunc(cluster) + case ICluster: + cluster := ap.specDiff.Added[path].(ICluster) + clusterFunc(cluster) + //case *api.ChiShard: + // shard := ap.specDiff.Added[path].(*api.ChiShard) + // shardFunc(shard) + case IShard: + shard := ap.specDiff.Added[path].(IShard) + shardFunc(shard) + case *Host: + host := ap.specDiff.Added[path].(*Host) + hostFunc(host) + } + } +} + +// WalkModified walk modified cluster items +func (ap *ActionPlan) WalkModified( + clusterFunc func(cluster ICluster), + shardFunc func(shard IShard), + hostFunc func(host *Host), +) { + if ap == nil { + return + } + // TODO refactor to map[string]object handling, instead of slice + for path := range ap.specDiff.Modified { + switch ap.specDiff.Modified[path].(type) { + //case api.ChiCluster: + // cluster := ap.specDiff.Modified[path].(api.ChiCluster) + // clusterFunc(&cluster) + //case api.ChiShard: + // shard := ap.specDiff.Modified[path].(api.ChiShard) + // shardFunc(&shard) + //case api.Host: + // host := ap.specDiff.Modified[path].(api.Host) + // hostFunc(&host) + //case *api.ChiCluster: + // cluster := ap.specDiff.Modified[path].(*api.ChiCluster) + // clusterFunc(cluster) + case ICluster: + cluster := ap.specDiff.Modified[path].(ICluster) + clusterFunc(cluster) + //case *ChiShard: + // shard := ap.specDiff.Modified[path].(*ChiShard) + // shardFunc(shard) + case IShard: + shard := ap.specDiff.Modified[path].(IShard) + shardFunc(shard) + case *Host: + host := ap.specDiff.Modified[path].(*Host) + hostFunc(host) + } + } +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/api_register.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/api_register.go new file mode 100644 index 00000000..b07ff282 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/api_register.go @@ -0,0 +1,54 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" + + clickhouse_altinity_com "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com" +) + +var ( + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{ + Group: clickhouse_altinity_com.APIGroupName, + Version: APIVersion, + } + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{ + GroupVersion: SchemeGroupVersion, + } + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +func init() { + SchemeBuilder.Register( + &ClickHouseInstallation{}, + &ClickHouseInstallationList{}, + &ClickHouseInstallationTemplate{}, + &ClickHouseInstallationTemplateList{}, + &ClickHouseOperatorConfiguration{}, + &ClickHouseOperatorConfigurationList{}, + ) +} + +// Resource returns schema.GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/api_resources.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/api_resources.go new file mode 100644 index 00000000..27e173ff --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/api_resources.go @@ -0,0 +1,26 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +k8s:deepcopy-gen=package,register +// +groupName=clickhouse.altinity.com + +// Package v1 defines version 1 of the API used with ClickHouse Installation Custom Resources. +package v1 + +// Possible kinds of CRDs +const ( + ClickHouseInstallationCRDResourceKind = "ClickHouseInstallation" + ClickHouseInstallationTemplateCRDResourceKind = "ClickHouseInstallationTemplate" + ClickHouseOperatorCRDResourceKind = "ClickHouseOperator" +) diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/api_version.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/api_version.go new file mode 100644 index 00000000..f1dcbe9e --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/api_version.go @@ -0,0 +1,20 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +const ( + // APIVersion is the version of the Clickhouse Operator API. + APIVersion = "v1" +) diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/cr.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/cr.go new file mode 100644 index 00000000..0955b378 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/cr.go @@ -0,0 +1,168 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "math" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/deployment" +) + +// getMaxNumberOfPodsPerNode calculates the max number of Pods allowed per Node +// Used for scope cycles calculation +// TODO need to support multi-cluster +func getMaxNumberOfPodsPerNode(cr ICustomResource) int { + maxNumberOfPodsPerNode := 0 + cr.WalkPodTemplates(func(template *PodTemplate) { + for i := range template.PodDistribution { + podDistribution := &template.PodDistribution[i] + if podDistribution.Type == deployment.PodDistributionMaxNumberPerNode { + maxNumberOfPodsPerNode = podDistribution.Number + } + } + }) + return maxNumberOfPodsPerNode +} + +// calcCRAndClusterScopeCycleSizes calculates scope cycle sizes +func calcCRAndClusterScopeCycleSizes(cr ICustomResource, maxNumberOfPodsPerNode int) (crScopeCycleSize int, clusterScopeCycleSize int) { + // 1perNode 2perNode 3perNode 4perNode 5perNode + // sh1r1 n1 a n1 a n1 a n1 a n1 a + // sh1r2 n2 a n2 a n2 a n2 a n2 a + // sh1r3 n3 a n3 a n3 a n3 a n3 a + // sh2r1 n4 a n4 a n4 a n4 a n1 b + // sh2r2 n5 a n5 a n5 a n1 b n2 b + // sh2r3 n6 a n6 a n1 b n2 b n3 b + // sh3r1 n7 a n7 a n2 b n3 b n1 c + // sh3r2 n8 a n8 a n3 b n4 b n2 c + // sh3r3 n9 a n1 b n4 b n1 c n3 c + // sh4r1 n10 a n2 b n5 b n2 c n1 d + // sh4r2 n11 a n3 b n1 c n3 c n2 d + // sh4r3 n12 a n4 b n2 c n4 c n3 d + // sh5r1 n13 a n5 b n3 c n1 d n1 e + // sh5r2 n14 a n6 b n4 c n2 d n2 e + // sh5r3 n15 a n7 b n5 c n3 d n3 e + // 1perNode = ceil(15 / 1 'cycles num') = 15 'cycle len' + // 2perNode = ceil(15 / 2 'cycles num') = 8 'cycle len' + // 3perNode = ceil(15 / 3 'cycles num') = 5 'cycle len' + // 4perNode = ceil(15 / 4 'cycles num') = 4 'cycle len' + // 5perNode = ceil(15 / 5 'cycles num') = 3 'cycle len' + + // Number of requested cycles equals to max number of ClickHouses per node, but can't be less than 1 + requestedClusterScopeCyclesNum := maxNumberOfPodsPerNode + if requestedClusterScopeCyclesNum <= 0 { + requestedClusterScopeCyclesNum = 1 + } + + crScopeCycleSize = 0 // Unlimited + clusterScopeCycleSize = 0 + if requestedClusterScopeCyclesNum == 1 { + // One cycle only requested + clusterScopeCycleSize = 0 // Unlimited + } else { + // Multiple cycles requested + clusterScopeCycleSize = int(math.Ceil(float64(cr.HostsCount()) / float64(requestedClusterScopeCyclesNum))) + } + + return crScopeCycleSize, clusterScopeCycleSize +} + +// fillSelfCalculatedAddressInfo calculates and fills address info +func fillSelfCalculatedAddressInfo(cr ICustomResource) { + // What is the max number of Pods allowed per Node + maxNumberOfPodsPerNode := getMaxNumberOfPodsPerNode(cr) + chiScopeCycleSize, clusterScopeCycleSize := calcCRAndClusterScopeCycleSizes(cr, maxNumberOfPodsPerNode) + + cr.WalkHostsFullPathAndScope( + chiScopeCycleSize, + clusterScopeCycleSize, + func( + cr ICustomResource, + cluster ICluster, + shard IShard, + replica IReplica, + host IHost, + address *types.HostScopeAddress, + ) error { + cluster.GetRuntime().GetAddress().SetNamespace(cr.GetNamespace()) + cluster.GetRuntime().GetAddress().SetCRName(cr.GetName()) + cluster.GetRuntime().GetAddress().SetClusterName(cluster.GetName()) + cluster.GetRuntime().GetAddress().SetClusterIndex(address.ClusterIndex) + + shard.GetRuntime().GetAddress().SetNamespace(cr.GetNamespace()) + shard.GetRuntime().GetAddress().SetCRName(cr.GetName()) + shard.GetRuntime().GetAddress().SetClusterName(cluster.GetName()) + shard.GetRuntime().GetAddress().SetClusterIndex(address.ClusterIndex) + shard.GetRuntime().GetAddress().SetShardName(shard.GetName()) + shard.GetRuntime().GetAddress().SetShardIndex(address.ShardIndex) + + replica.GetRuntime().GetAddress().SetNamespace(cr.GetNamespace()) + replica.GetRuntime().GetAddress().SetCRName(cr.GetName()) + replica.GetRuntime().GetAddress().SetClusterName(cluster.GetName()) + replica.GetRuntime().GetAddress().SetClusterIndex(address.ClusterIndex) + replica.GetRuntime().GetAddress().SetReplicaName(replica.GetName()) + replica.GetRuntime().GetAddress().SetReplicaIndex(address.ReplicaIndex) + + host.GetRuntime().GetAddress().SetNamespace(cr.GetNamespace()) + // Skip StatefulSet as impossible to self-calculate + // host.Address.StatefulSet = CreateStatefulSetName(host) + host.GetRuntime().GetAddress().SetCRName(cr.GetName()) + host.GetRuntime().GetAddress().SetClusterName(cluster.GetName()) + host.GetRuntime().GetAddress().SetClusterIndex(address.ClusterIndex) + host.GetRuntime().GetAddress().SetShardName(shard.GetName()) + host.GetRuntime().GetAddress().SetShardIndex(address.ShardIndex) + host.GetRuntime().GetAddress().SetReplicaName(replica.GetName()) + host.GetRuntime().GetAddress().SetReplicaIndex(address.ReplicaIndex) + host.GetRuntime().GetAddress().SetHostName(host.GetName()) + host.GetRuntime().GetAddress().SetCRScopeIndex(address.CRScopeAddress.Index) + host.GetRuntime().GetAddress().SetCRScopeCycleSize(address.CRScopeAddress.CycleSpec.Size) + host.GetRuntime().GetAddress().SetCRScopeCycleIndex(address.CRScopeAddress.CycleAddress.CycleIndex) + host.GetRuntime().GetAddress().SetCRScopeCycleOffset(address.CRScopeAddress.CycleAddress.Index) + host.GetRuntime().GetAddress().SetClusterScopeIndex(address.ClusterScopeAddress.Index) + host.GetRuntime().GetAddress().SetClusterScopeCycleSize(address.ClusterScopeAddress.CycleSpec.Size) + host.GetRuntime().GetAddress().SetClusterScopeCycleIndex(address.ClusterScopeAddress.CycleAddress.CycleIndex) + host.GetRuntime().GetAddress().SetClusterScopeCycleOffset(address.ClusterScopeAddress.CycleAddress.Index) + host.GetRuntime().GetAddress().SetShardScopeIndex(address.ReplicaIndex) + host.GetRuntime().GetAddress().SetReplicaScopeIndex(address.ShardIndex) + + return nil + }, + ) +} + +func fillCRPointer(cr ICustomResource) { + cr.WalkHostsFullPath( + func( + cr ICustomResource, + cluster ICluster, + shard IShard, + replica IReplica, + host IHost, + address *types.HostScopeAddress, + ) error { + cluster.GetRuntime().SetCR(cr) + shard.GetRuntime().SetCR(cr) + replica.GetRuntime().SetCR(cr) + host.GetRuntime().SetCR(cr) + return nil + }, + ) +} + +func FillCR(cr ICustomResource) { + fillSelfCalculatedAddressInfo(cr) + fillCRPointer(cr) +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/doc.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/doc.go new file mode 100644 index 00000000..28cb439e --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/doc.go @@ -0,0 +1,19 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +k8s:deepcopy-gen=package,register +// +groupName=clickhouse.altinity.com + +// Package v1 defines version 1 of the API used with ClickHouse Installation Custom Resources. +package v1 diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/interface.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/interface.go new file mode 100644 index 00000000..af526739 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/interface.go @@ -0,0 +1,309 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "fmt" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +type ICustomResource interface { + meta.Object + + NamespaceName() (string, string) + + IsNonZero() bool + IsZero() bool + + GetSpecA() any + GetSpec() ICRSpec + GetRuntime() ICustomResourceRuntime + GetRootServiceTemplates() ([]*ServiceTemplate, bool) + GetReconcile() *ChiReconcile + + WalkClusters(f func(cluster ICluster) error) []error + WalkHosts(func(host *Host) error) []error + WalkPodTemplates(f func(template *PodTemplate)) + WalkVolumeClaimTemplates(f func(template *VolumeClaimTemplate)) + WalkHostsFullPath(f WalkHostsAddressFn) []error + WalkHostsFullPathAndScope(crScopeCycleSize int, clusterScopeCycleSize int, f WalkHostsAddressFn) (res []error) + + FindCluster(needle interface{}) ICluster + FindShard(needleCluster interface{}, needleShard interface{}) IShard + FindHost(needleCluster interface{}, needleShard interface{}, needleHost interface{}) *Host + + GetHostTemplate(name string) (*HostTemplate, bool) + GetPodTemplate(name string) (*PodTemplate, bool) + GetVolumeClaimTemplate(name string) (*VolumeClaimTemplate, bool) + GetServiceTemplate(name string) (*ServiceTemplate, bool) + + HasAncestor() bool + GetAncestor() ICustomResource + + IsStopped() bool + IsTroubleshoot() bool + IsRollingUpdate() bool + + HostsCount() int + IEnsureStatus() IStatus + GetStatus() IStatus + + YAML(opts types.CopyCROptions) string +} + +type ICRSpec interface { + GetNamespaceDomainPattern() *types.String + GetDefaults() *Defaults + GetConfiguration() IConfiguration + GetTaskID() *types.Id +} + +type IConfiguration interface { + GetUsers() *Settings + GetProfiles() *Settings + GetQuotas() *Settings + GetSettings() *Settings + GetFiles() *Settings +} + +type ICustomResourceRuntime interface { + GetAttributes() *ComparableAttributes + LockCommonConfig() + UnlockCommonConfig() +} + +type IStatus interface { + SetAction(string) + PushAction(string) + SetError(string) + PushError(string) + GetHostsCount() int + GetHostsCompletedCount() int + GetHostsAddedCount() int + GetHostsWithTablesCreated() []string + GetHostsWithReplicaCaughtUp() []string + PushHostTablesCreated(host string) + PushHostReplicaCaughtUp(host string) + + HasNormalizedCRCompleted() bool + + HostUnchanged() + HostUpdated() + HostAdded() + HostFailed() + HostCompleted() +} + +type ICluster interface { + IsNonZero() bool + IsZero() bool + + GetName() string + HasName() bool + GetZookeeper() *ZookeeperConfig + GetSchemaPolicy() *SchemaPolicy + GetInsecure() *types.StringBool + GetSecure() *types.StringBool + GetSecret() *ClusterSecret + GetPDBManaged() *types.StringBool + GetPDBMaxUnavailable() *types.Int32 + + WalkShards(f func(index int, shard IShard) error) []error + WalkHosts(func(host *Host) error) []error + + HostsCount() int + + FindShard(needle interface{}) IShard + FindHost(needleShard interface{}, needleHost interface{}) *Host + + SelectSettingsSourceFrom(shard IShard, replica IReplica) any + + GetRuntime() IClusterRuntime + GetReconcile() *ClusterReconcile + GetServiceTemplate() (*ServiceTemplate, bool) + GetAncestor() ICluster +} + +type IClusterRuntime interface { + GetAddress() IClusterAddress + GetCR() ICustomResource + SetCR(cr ICustomResource) +} + +type IClusterAddress interface { + GetNamespace() string + SetNamespace(string) + + GetCRName() string + SetCRName(string) + + GetClusterName() string + SetClusterName(string) + + GetClusterIndex() int + SetClusterIndex(int) +} + +type IShard interface { + IsNonZero() bool + IsZero() bool + + GetName() string + GetRuntime() IShardRuntime + GetServiceTemplate() (*ServiceTemplate, bool) + GetInternalReplication() *types.StringBool + HasWeight() bool + GetWeight() int + HasSettings() bool + GetSettings() *Settings + HasFiles() bool + GetFiles() *Settings + HasTemplates() bool + GetTemplates() *TemplatesList + + WalkHosts(func(host *Host) error) []error + WalkHostsAbortOnError(f func(host *Host) error) error + + FindHost(needleHost interface{}) *Host + FirstHost() *Host + + HostsCount() int + GetAncestor() IShard +} + +type IShardRuntime interface { + GetAddress() IShardAddress + GetCR() ICustomResource + SetCR(cr ICustomResource) +} + +type IShardAddress interface { + IClusterAddress + + GetShardName() string + SetShardName(string) + + GetShardIndex() int + SetShardIndex(int) +} + +type IReplica interface { + GetName() string + GetRuntime() IReplicaRuntime + HasSettings() bool + GetSettings() *Settings + HasFiles() bool + GetFiles() *Settings + HasTemplates() bool + GetTemplates() *TemplatesList +} + +type IReplicaRuntime interface { + GetAddress() IReplicaAddress + SetCR(cr ICustomResource) +} + +type IReplicaAddress interface { + IClusterAddress + + GetReplicaName() string + SetReplicaName(string) + + GetReplicaIndex() int + SetReplicaIndex(int) +} + +type IHost interface { + GetName() string + GetRuntime() IHostRuntime +} + +type IHostRuntime interface { + GetAddress() IHostAddress + GetCR() ICustomResource + SetCR(cr ICustomResource) +} + +type IHostAddress interface { + IReplicaAddress + IShardAddress + + GetStatefulSet() string + GetFQDN() string + + GetHostName() string + SetHostName(string) + + GetCRScopeIndex() int + SetCRScopeIndex(int) + GetCRScopeCycleSize() int + SetCRScopeCycleSize(int) + GetCRScopeCycleIndex() int + SetCRScopeCycleIndex(int) + GetCRScopeCycleOffset() int + SetCRScopeCycleOffset(int) + GetClusterScopeIndex() int + SetClusterScopeIndex(int) + GetClusterScopeCycleSize() int + SetClusterScopeCycleSize(int) + GetClusterScopeCycleIndex() int + SetClusterScopeCycleIndex(int) + GetClusterScopeCycleOffset() int + SetClusterScopeCycleOffset(int) + GetShardScopeIndex() int + SetShardScopeIndex(int) + GetReplicaScopeIndex() int + SetReplicaScopeIndex(int) +} + +// WalkHostsAddressFn specifies function to walk over hosts +type WalkHostsAddressFn func( + cr ICustomResource, + cluster ICluster, + shard IShard, + replica IReplica, + host IHost, + address *types.HostScopeAddress, +) error + +type IGenerateName interface { + HasGenerateName() bool + GetGenerateName() string +} + +type IActionPlan interface { + fmt.Stringer + HasActionsToDo() bool + GetRemovedHostsNum() int + Log(tag string) string + WalkRemoved( + clusterFunc func(cluster ICluster), + shardFunc func(shard IShard), + hostFunc func(host *Host), + ) + WalkAdded( + clusterFunc func(cluster ICluster), + shardFunc func(shard IShard), + hostFunc func(host *Host), + ) + WalkModified( + clusterFunc func(cluster ICluster), + shardFunc func(shard IShard), + hostFunc func(host *Host), + ) +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_attributes.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_attributes.go new file mode 100644 index 00000000..c7643ee3 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_attributes.go @@ -0,0 +1,144 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import core "k8s.io/api/core/v1" + +// ComparableAttributes specifies CHI attributes that are comparable +type ComparableAttributes struct { + additionalEnvVars []core.EnvVar `json:"-" yaml:"-"` + additionalVolumes []core.Volume `json:"-" yaml:"-"` + additionalVolumeMounts []core.VolumeMount `json:"-" yaml:"-"` + skipOwnerRef bool `json:"-" yaml:"-"` +} + +func (a *ComparableAttributes) GetAdditionalEnvVars() []core.EnvVar { + if a == nil { + return nil + } + return a.additionalEnvVars +} + +func (a *ComparableAttributes) AppendAdditionalEnvVar(envVar core.EnvVar) { + if a == nil { + return + } + a.additionalEnvVars = append(a.additionalEnvVars, envVar) +} + +func (a *ComparableAttributes) AppendAdditionalEnvVarIfNotExists(envVar core.EnvVar) { + if a == nil { + return + } + + // Sanity check + if envVar.Name == "" { + // This env var is incorrect + return + } + + for _, existingEnvVar := range a.GetAdditionalEnvVars() { + if existingEnvVar.Name == envVar.Name { + // Such a variable already exists + return + } + } + + a.AppendAdditionalEnvVar(envVar) +} + +func (a *ComparableAttributes) GetAdditionalVolumes() []core.Volume { + if a == nil { + return nil + } + return a.additionalVolumes +} + +func (a *ComparableAttributes) AppendAdditionalVolume(volume core.Volume) { + if a == nil { + return + } + a.additionalVolumes = append(a.additionalVolumes, volume) +} + +func (a *ComparableAttributes) AppendAdditionalVolumeIfNotExists(volume core.Volume) { + if a == nil { + return + } + + // Sanity check + if volume.Name == "" { + // This volume is incorrect + return + } + + for _, existingVolume := range a.GetAdditionalVolumes() { + if existingVolume.Name == volume.Name { + // Such a volume already exists + return + } + } + + // Volume looks good + a.AppendAdditionalVolume(volume) +} + +func (a *ComparableAttributes) GetAdditionalVolumeMounts() []core.VolumeMount { + if a == nil { + return nil + } + return a.additionalVolumeMounts +} + +func (a *ComparableAttributes) AppendAdditionalVolumeMount(volumeMount core.VolumeMount) { + if a == nil { + return + } + a.additionalVolumeMounts = append(a.additionalVolumeMounts, volumeMount) +} + +func (a *ComparableAttributes) AppendAdditionalVolumeMountIfNotExists(volumeMount core.VolumeMount) { + if a == nil { + return + } + + // Sanity check + if volumeMount.Name == "" { + return + } + + for _, existingVolumeMount := range a.GetAdditionalVolumeMounts() { + if existingVolumeMount.Name == volumeMount.Name { + // Such a volume mount already exists + return + } + } + + a.AppendAdditionalVolumeMount(volumeMount) +} + +func (a *ComparableAttributes) GetSkipOwnerRef() bool { + if a == nil { + return false + } + return a.skipOwnerRef +} + +func (a *ComparableAttributes) SetSkipOwnerRef(skip bool) { + if a == nil { + return + } + a.skipOwnerRef = skip +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_chi.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_chi.go new file mode 100644 index 00000000..6aefba10 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_chi.go @@ -0,0 +1,740 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/swversion" + + "github.com/imdario/mergo" + "gopkg.in/yaml.v3" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" +) + +func (cr *ClickHouseInstallation) GetSpec() ICRSpec { + return &cr.Spec +} + +func (cr *ClickHouseInstallation) GetSpecT() *ChiSpec { + return &cr.Spec +} + +func (cr *ClickHouseInstallation) GetSpecA() any { + return &cr.Spec +} + +func (cr *ClickHouseInstallation) GetRuntime() ICustomResourceRuntime { + return cr.EnsureRuntime() +} + +func (cr *ClickHouseInstallation) EnsureRuntime() *ClickHouseInstallationRuntime { + if cr == nil { + return nil + } + + // Assume that most of the time, we'll see a non-nil value. + if cr.runtime != nil { + return cr.runtime + } + + // Otherwise, we need to acquire a lock to initialize the field. + cr.runtimeCreatorMutex.Lock() + defer cr.runtimeCreatorMutex.Unlock() + // Note that we have to check this property again to avoid a TOCTOU bug. + if cr.runtime == nil { + cr.runtime = newClickHouseInstallationRuntime() + } + return cr.runtime +} + +func (cr *ClickHouseInstallation) IEnsureStatus() IStatus { + return any(cr.EnsureStatus()).(IStatus) +} + +// EnsureStatus ensures status +func (cr *ClickHouseInstallation) EnsureStatus() *Status { + if cr == nil { + return nil + } + + // Assume that most of the time, we'll see a non-nil value. + if cr.Status != nil { + return cr.Status + } + + // Otherwise, we need to acquire a lock to initialize the field. + cr.statusCreatorMutex.Lock() + defer cr.statusCreatorMutex.Unlock() + // Note that we have to check this property again to avoid a TOCTOU bug. + if cr.Status == nil { + cr.Status = &Status{} + } + return cr.Status +} + +// GetStatus gets Status +func (cr *ClickHouseInstallation) GetStatus() IStatus { + if cr == nil { + return (*Status)(nil) + } + return cr.Status +} + +// HasStatus checks whether CHI has Status +func (cr *ClickHouseInstallation) HasStatus() bool { + if cr == nil { + return false + } + return cr.Status != nil +} + +// HasAncestor checks whether CR has an ancestor +func (cr *ClickHouseInstallation) HasAncestor() bool { + if !cr.HasStatus() { + return false + } + return cr.Status.HasNormalizedCRCompleted() +} + +// GetAncestor gets ancestor of a CR +func (cr *ClickHouseInstallation) GetAncestor() ICustomResource { + if !cr.HasAncestor() { + return (*ClickHouseInstallation)(nil) + } + return cr.Status.GetNormalizedCRCompleted() +} + +// GetAncestorT gets ancestor of a CR +func (cr *ClickHouseInstallation) GetAncestorT() *ClickHouseInstallation { + if !cr.HasAncestor() { + return nil + } + return cr.Status.GetNormalizedCRCompleted() +} + +// SetAncestor sets ancestor of a CR +func (cr *ClickHouseInstallation) SetAncestor(a *ClickHouseInstallation) { + if cr == nil { + return + } + cr.EnsureStatus().NormalizedCRCompleted = a +} + +// HasTarget checks whether CR has a target +func (cr *ClickHouseInstallation) HasTarget() bool { + if !cr.HasStatus() { + return false + } + return cr.Status.HasNormalizedCR() +} + +// GetTarget gets target of a CR +func (cr *ClickHouseInstallation) GetTarget() *ClickHouseInstallation { + if !cr.HasTarget() { + return nil + } + return cr.Status.GetNormalizedCR() +} + +// SetTarget sets target of a CR +func (cr *ClickHouseInstallation) SetTarget(a *ClickHouseInstallation) { + if cr == nil { + return + } + cr.EnsureStatus().NormalizedCR = a +} + +func (cr *ClickHouseInstallation) GetUsedTemplates() []*TemplateRef { + return cr.GetSpecT().UseTemplates +} + +// FillStatus fills .Status +func (cr *ClickHouseInstallation) FillStatus(endpoints util.Slice[string], pods, fqdns []string, ip string) { + cr.EnsureStatus().Fill(&FillStatusParams{ + CHOpIP: ip, + ClustersCount: cr.ClustersCount(), + ShardsCount: cr.ShardsCount(), + HostsCount: cr.HostsCount(), + TaskID: cr.GetSpecT().GetTaskID().Value(), + HostsUpdatedCount: 0, + HostsAddedCount: 0, + HostsUnchangedCount: 0, + HostsCompletedCount: 0, + HostsDeleteCount: 0, + HostsDeletedCount: 0, + Pods: pods, + FQDNs: fqdns, + Endpoint: endpoints.First(), + Endpoints: append([]string{}, endpoints...), + NormalizedCR: cr.Copy(types.CopyCROptions{ + SkipStatus: true, + SkipManagedFields: true, + }), + }) +} + +func (cr *ClickHouseInstallation) Fill() { + FillCR(cr) +} + +// MergeFrom merges from CHI +func (cr *ClickHouseInstallation) MergeFrom(from *ClickHouseInstallation, _type MergeType) { + if from == nil { + return + } + + // Merge Meta + switch _type { + case MergeTypeFillEmptyValues: + _ = mergo.Merge(&cr.TypeMeta, from.TypeMeta) + _ = mergo.Merge(&cr.ObjectMeta, from.ObjectMeta) + case MergeTypeOverrideByNonEmptyValues: + _ = mergo.Merge(&cr.TypeMeta, from.TypeMeta, mergo.WithOverride) + _ = mergo.Merge(&cr.ObjectMeta, from.ObjectMeta, mergo.WithOverride) + } + // Exclude skipped annotations + cr.SetAnnotations( + util.CopyMapFilter( + cr.GetAnnotations(), + nil, + util.ListSkippedAnnotations(), + ), + ) + + // Do actual merge for Spec + cr.GetSpecT().MergeFrom(from.GetSpecT(), _type) + + // Copy service attributes + cr.EnsureRuntime().attributes = from.EnsureRuntime().attributes + + cr.EnsureStatus().CopyFrom(from.Status, types.CopyStatusOptions{ + CopyStatusFieldGroup: types.CopyStatusFieldGroup{ + FieldGroupInheritable: true, + }, + }) +} + +// FindCluster finds cluster by name or index. +// Expectations: name is expected to be a string, index is expected to be an int. +func (cr *ClickHouseInstallation) FindCluster(needle interface{}) ICluster { + var resultCluster *Cluster + cr.WalkClustersFullPath(func(chi *ClickHouseInstallation, clusterIndex int, cluster *Cluster) error { + switch v := needle.(type) { + case string: + if cluster.Name == v { + resultCluster = cluster + } + case int: + if clusterIndex == v { + resultCluster = cluster + } + } + return nil + }) + return resultCluster +} + +// FindShard finds shard by name or index +// Expectations: name is expected to be a string, index is expected to be an int. +func (cr *ClickHouseInstallation) FindShard(needleCluster interface{}, needleShard interface{}) IShard { + return cr.FindCluster(needleCluster).FindShard(needleShard) +} + +// FindHost finds shard by name or index +// Expectations: name is expected to be a string, index is expected to be an int. +func (cr *ClickHouseInstallation) FindHost(needleCluster interface{}, needleShard interface{}, needleHost interface{}) *Host { + return cr.FindCluster(needleCluster).FindHost(needleShard, needleHost) +} + +// ClustersCount counts clusters +func (cr *ClickHouseInstallation) ClustersCount() int { + count := 0 + cr.WalkClusters(func(cluster ICluster) error { + count++ + return nil + }) + return count +} + +// ShardsCount counts shards +func (cr *ClickHouseInstallation) ShardsCount() int { + count := 0 + cr.WalkShards(func(shard *ChiShard) error { + count++ + return nil + }) + return count +} + +// HostsCount counts hosts +func (cr *ClickHouseInstallation) HostsCount() int { + count := 0 + cr.WalkHosts(func(host *Host) error { + count++ + return nil + }) + return count +} + +// HostsWithAttributesCount counts hosts by attributes +func (cr *ClickHouseInstallation) HostsWithAttributesCount(a *types.ReconcileAttributes) int { + count := 0 + cr.WalkHosts(func(host *Host) error { + if host.GetReconcileAttributes().HasIntersectionWith(a) { + count++ + } + return nil + }) + return count +} + +// HasReconcileWork reports whether the CR has any work to reconcile: +// either the ActionPlan has spec/label/finalizer changes, or child resources have drifted. +func (cr *ClickHouseInstallation) HasReconcileWork() bool { + return cr.EnsureRuntime().ActionPlan.HasActionsToDo() || cr.GetHostsAttributesCounters().HasDrift() +} + +// GetHostsAttributesCounters +func (cr *ClickHouseInstallation) GetHostsAttributesCounters() *types.ReconcileAttributesCounters { + counters := types.NewReconcileAttributesCounters() + cr.WalkHosts(func(host *Host) error { + counters.Add(host.GetReconcileAttributes()) + return nil + }) + return counters +} + +// GetHostTemplate gets HostTemplate by name +func (cr *ClickHouseInstallation) GetHostTemplate(name string) (*HostTemplate, bool) { + if !cr.GetSpecT().GetTemplates().GetHostTemplatesIndex().Has(name) { + return nil, false + } + return cr.GetSpecT().GetTemplates().GetHostTemplatesIndex().Get(name), true +} + +// GetPodTemplate gets PodTemplate by name +func (cr *ClickHouseInstallation) GetPodTemplate(name string) (*PodTemplate, bool) { + if !cr.GetSpecT().GetTemplates().GetPodTemplatesIndex().Has(name) { + return nil, false + } + return cr.GetSpecT().GetTemplates().GetPodTemplatesIndex().Get(name), true +} + +// WalkPodTemplates walks over all PodTemplates +func (cr *ClickHouseInstallation) WalkPodTemplates(f func(template *PodTemplate)) { + cr.GetSpecT().GetTemplates().GetPodTemplatesIndex().Walk(f) +} + +// GetVolumeClaimTemplate gets VolumeClaimTemplate by name +func (cr *ClickHouseInstallation) GetVolumeClaimTemplate(name string) (*VolumeClaimTemplate, bool) { + if cr.GetSpecT().GetTemplates().GetVolumeClaimTemplatesIndex().Has(name) { + return cr.GetSpecT().GetTemplates().GetVolumeClaimTemplatesIndex().Get(name), true + } + return nil, false +} + +// WalkVolumeClaimTemplates walks over all VolumeClaimTemplates +func (cr *ClickHouseInstallation) WalkVolumeClaimTemplates(f func(template *VolumeClaimTemplate)) { + if cr == nil { + return + } + cr.GetSpecT().GetTemplates().GetVolumeClaimTemplatesIndex().Walk(f) +} + +// GetServiceTemplate gets ServiceTemplate by name +func (cr *ClickHouseInstallation) GetServiceTemplate(name string) (*ServiceTemplate, bool) { + if !cr.GetSpecT().GetTemplates().GetServiceTemplatesIndex().Has(name) { + return nil, false + } + return cr.GetSpecT().GetTemplates().GetServiceTemplatesIndex().Get(name), true +} + +// GetServiceTemplates gets ServiceTemplates by name +func (cr *ClickHouseInstallation) GetServiceTemplates(names ...string) ([]*ServiceTemplate, bool) { + if len(names) == 0 { + return nil, false + } + var res []*ServiceTemplate + for _, name := range names { + if cr.GetSpecT().GetTemplates().GetServiceTemplatesIndex().Has(name) { + res = append(res, cr.GetSpecT().GetTemplates().GetServiceTemplatesIndex().Get(name)) + } + } + if len(res) == len(names) { + return res, true + } + return nil, false +} + +// GetRootServiceTemplates gets service templates of a CR +func (cr *ClickHouseInstallation) GetRootServiceTemplates() ([]*ServiceTemplate, bool) { + if !cr.GetSpecT().GetDefaults().Templates.HasAnyServiceTemplate() { + return nil, false + } + return cr.GetServiceTemplates(cr.GetSpecT().GetDefaults().Templates.GetAllServiceTemplates()...) +} + +// MatchNamespace matches namespace +func (cr *ClickHouseInstallation) MatchNamespace(namespace string) bool { + if cr == nil { + return false + } + return cr.Namespace == namespace +} + +// MatchFullName matches full name +func (cr *ClickHouseInstallation) MatchFullName(namespace, name string) bool { + if cr == nil { + return false + } + return (cr.Namespace == namespace) && (cr.Name == name) +} + +// FoundIn checks whether CHI can be found in haystack +func (cr *ClickHouseInstallation) FoundIn(haystack []*ClickHouseInstallation) bool { + if cr == nil { + return false + } + + for _, candidate := range haystack { + if candidate.MatchFullName(cr.Namespace, cr.Name) { + return true + } + } + + return false +} + +// Possible templating policies +const ( + TemplatingPolicyAuto = "auto" + TemplatingPolicyManual = "manual" +) + +// IsAuto checks whether templating policy is auto +func (cr *ClickHouseInstallation) IsAuto() bool { + if cr == nil { + return false + } + if (cr.Namespace == "") && (cr.Name == "") { + return false + } + return cr.GetSpecT().GetTemplating().GetPolicy() == TemplatingPolicyAuto +} + +// IsStopped checks whether CR is stopped +func (cr *ClickHouseInstallation) IsStopped() bool { + if cr == nil { + return false + } + return cr.GetSpecT().GetStop().Value() +} + +// Restart constants present available values for .spec.restart +// Controlling the operator's Clickhouse instances restart policy +const ( + // RestartRollingUpdate requires to roll over all hosts in the cluster and shutdown and reconcile each of it. + // This restart policy means that all hosts in the cluster would pass through shutdown/reconcile cycle. + RestartRollingUpdate = "RollingUpdate" +) + +// IsRollingUpdate checks whether CHI should perform rolling update +func (cr *ClickHouseInstallation) IsRollingUpdate() bool { + if cr == nil { + return false + } + return cr.GetSpecT().GetRestart().Value() == RestartRollingUpdate +} + +// IsTroubleshoot checks whether CHI is in troubleshoot mode +func (cr *ClickHouseInstallation) IsTroubleshoot() bool { + if cr == nil { + return false + } + return cr.GetSpecT().GetTroubleshoot().Value() +} + +// GetReconcile gets reconcile spec +func (cr *ClickHouseInstallation) GetReconcile() *ChiReconcile { + if cr == nil { + return nil + } + return cr.GetSpecT().Reconcile +} + +// Copy makes copy of a CHI, filtering fields according to specified CopyOptions +func (cr *ClickHouseInstallation) Copy(opts types.CopyCROptions) *ClickHouseInstallation { + if cr == nil { + return nil + } + jsonBytes, err := json.Marshal(cr) + if err != nil { + return nil + } + + var cr2 *ClickHouseInstallation + if err := json.Unmarshal(jsonBytes, &cr2); err != nil { + return nil + } + + if opts.SkipStatus { + cr2.Status = nil + } + + if opts.SkipManagedFields { + cr2.SetManagedFields(nil) + } + + return cr2 +} + +// JSON returns JSON string +func (cr *ClickHouseInstallation) JSON(opts types.CopyCROptions) string { + if cr == nil { + return "" + } + + filtered := cr.Copy(opts) + jsonBytes, err := json.MarshalIndent(filtered, "", " ") + if err != nil { + return fmt.Sprintf("unable to parse. err: %v", err) + } + return string(jsonBytes) + +} + +// YAML return YAML string +func (cr *ClickHouseInstallation) YAML(opts types.CopyCROptions) string { + if cr == nil { + return "" + } + + filtered := cr.Copy(opts) + yamlBytes, err := yaml.Marshal(filtered) + if err != nil { + return fmt.Sprintf("unable to parse. err: %v", err) + } + return string(yamlBytes) +} + +// FirstHost returns first host of the CR +func (cr *ClickHouseInstallation) FirstHost() *Host { + var result *Host + cr.WalkHosts(func(host *Host) error { + if result == nil { + result = host + } + return nil + }) + return result +} + +func (cr *ClickHouseInstallation) GetName() string { + if cr == nil { + return "" + } + return cr.Name +} + +func (cr *ClickHouseInstallation) GetNamespace() string { + if cr == nil { + return "" + } + return cr.Namespace +} + +func (cr *ClickHouseInstallation) GetLabels() map[string]string { + if cr == nil { + return nil + } + return cr.Labels +} + +func (cr *ClickHouseInstallation) GetAnnotations() map[string]string { + if cr == nil { + return nil + } + return cr.Annotations +} + +// WalkClustersFullPath walks clusters with full path +func (cr *ClickHouseInstallation) WalkClustersFullPath( + f func(chi *ClickHouseInstallation, clusterIndex int, cluster *Cluster) error, +) []error { + if cr == nil { + return nil + } + res := make([]error, 0) + + for clusterIndex := range cr.GetSpecT().Configuration.Clusters { + res = append(res, f(cr, clusterIndex, cr.GetSpecT().Configuration.Clusters[clusterIndex])) + } + + return res +} + +// WalkClusters walks clusters +func (cr *ClickHouseInstallation) WalkClusters(f func(i ICluster) error) []error { + if cr == nil { + return nil + } + res := make([]error, 0) + + for clusterIndex := range cr.GetSpecT().Configuration.Clusters { + res = append(res, f(cr.GetSpecT().Configuration.Clusters[clusterIndex])) + } + + return res +} + +// WalkShards walks shards +func (cr *ClickHouseInstallation) WalkShards( + f func( + shard *ChiShard, + ) error, +) []error { + if cr == nil { + return nil + } + res := make([]error, 0) + + for clusterIndex := range cr.GetSpecT().Configuration.Clusters { + cluster := cr.GetSpecT().Configuration.Clusters[clusterIndex] + for shardIndex := range cluster.Layout.Shards { + shard := cluster.Layout.Shards[shardIndex] + res = append(res, f(shard)) + } + } + + return res +} + +// WalkHostsFullPathAndScope walks hosts with full path +func (cr *ClickHouseInstallation) WalkHostsFullPathAndScope( + crScopeCycleSize int, + clusterScopeCycleSize int, + f WalkHostsAddressFn, +) (res []error) { + if cr == nil { + return nil + } + address := types.NewHostScopeAddress(crScopeCycleSize, clusterScopeCycleSize) + for clusterIndex := range cr.GetSpecT().Configuration.Clusters { + cluster := cr.GetSpecT().Configuration.Clusters[clusterIndex] + address.ClusterScopeAddress.Init() + for shardIndex := range cluster.Layout.Shards { + shard := cluster.GetShard(shardIndex) + for replicaIndex, host := range shard.Hosts { + replica := cluster.GetReplica(replicaIndex) + address.ClusterIndex = clusterIndex + address.ShardIndex = shardIndex + address.ReplicaIndex = replicaIndex + res = append(res, f(cr, cluster, shard, replica, host, address)) + address.CRScopeAddress.Inc() + address.ClusterScopeAddress.Inc() + } + } + } + return res +} + +// WalkHostsFullPath walks hosts with a function +func (cr *ClickHouseInstallation) WalkHostsFullPath(f WalkHostsAddressFn) []error { + return cr.WalkHostsFullPathAndScope(0, 0, f) +} + +// WalkHosts walks hosts with a function +func (cr *ClickHouseInstallation) WalkHosts(f func(host *Host) error) []error { + if cr == nil { + return nil + } + res := make([]error, 0) + + for clusterIndex := range cr.GetSpecT().Configuration.Clusters { + cluster := cr.GetSpecT().Configuration.Clusters[clusterIndex] + for shardIndex := range cluster.Layout.Shards { + shard := cluster.Layout.Shards[shardIndex] + for replicaIndex := range shard.Hosts { + host := shard.Hosts[replicaIndex] + res = append(res, f(host)) + } + } + } + + return res +} + +// WalkTillError walks hosts with a function until an error met +func (cr *ClickHouseInstallation) WalkTillError( + ctx context.Context, + fCRPreliminary func(ctx context.Context, chi *ClickHouseInstallation) error, + fCluster func(ctx context.Context, cluster *Cluster) error, + fCRFinal func(ctx context.Context, chi *ClickHouseInstallation) error, +) error { + if err := fCRPreliminary(ctx, cr); err != nil { + return err + } + + for clusterIndex := range cr.GetSpecT().Configuration.Clusters { + cluster := cr.GetSpecT().Configuration.Clusters[clusterIndex] + if err := fCluster(ctx, cluster); err != nil { + return err + } + } + + if err := fCRFinal(ctx, cr); err != nil { + return err + } + + return nil +} + +func (cr *ClickHouseInstallation) IsZero() bool { + return cr == nil +} + +func (cr *ClickHouseInstallation) IsNonZero() bool { + return cr != nil +} + +func (cr *ClickHouseInstallation) NamespaceName() (string, string) { + return util.NamespaceName(cr) +} + +func (cr *ClickHouseInstallation) FindMinMaxVersions() { + cr.runtime.MinVersion = swversion.MaxVersion() + cr.runtime.MaxVersion = swversion.MinVersion() + cr.WalkHosts(func(host *Host) error { + if host.Runtime.Version.Cmp(cr.runtime.MinVersion) < 0 { + cr.runtime.MinVersion = host.Runtime.Version + } + if host.Runtime.Version.Cmp(cr.runtime.MaxVersion) > 0 { + cr.runtime.MaxVersion = host.Runtime.Version + } + return nil + }) +} + +func (cr *ClickHouseInstallation) GetMinVersion() *swversion.SoftWareVersion { + return cr.runtime.MinVersion +} + +func (cr *ClickHouseInstallation) GetMaxVersion() *swversion.SoftWareVersion { + return cr.runtime.MaxVersion +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_chi_templating.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_chi_templating.go new file mode 100644 index 00000000..0e3deef9 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_chi_templating.go @@ -0,0 +1,82 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// ChiTemplating defines templating policy struct +type ChiTemplating struct { + Policy string `json:"policy,omitempty" yaml:"policy,omitempty"` + CHISelector TargetSelector `json:"chiSelector,omitempty" yaml:"chiSelector,omitempty"` +} + +// NewChiTemplating creates new templating +func NewChiTemplating() *ChiTemplating { + return new(ChiTemplating) +} + +// GetPolicy gets policy +func (t *ChiTemplating) GetPolicy() string { + if t == nil { + return "" + } + return t.Policy +} + +// SetPolicy sets policy +func (t *ChiTemplating) SetPolicy(p string) { + if t == nil { + return + } + t.Policy = p +} + +// GetSelector gets CHI selector +func (t *ChiTemplating) GetSelector() TargetSelector { + if t == nil { + return nil + } + return t.CHISelector +} + +// MergeFrom merges from specified templating +func (t *ChiTemplating) MergeFrom(from *ChiTemplating, _type MergeType) *ChiTemplating { + if from == nil { + return t + } + + if t == nil { + t = NewChiTemplating() + } + + switch _type { + case MergeTypeFillEmptyValues: + if t.Policy == "" { + t.Policy = from.Policy + } + if t.CHISelector == nil { + t.CHISelector = from.CHISelector + } + case MergeTypeOverrideByNonEmptyValues: + if from.Policy != "" { + // Override by non-empty values only + t.Policy = from.Policy + } + if from.CHISelector != nil { + // Override by non-empty values only + t.CHISelector = from.CHISelector + } + } + + return t +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_cleanup.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_cleanup.go new file mode 100644 index 00000000..d83c4dc0 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_cleanup.go @@ -0,0 +1,252 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// Cleanup defines cleanup +type Cleanup struct { + // UnknownObjects specifies cleanup of unknown objects + UnknownObjects *ObjectsCleanup `json:"unknownObjects,omitempty" yaml:"unknownObjects,omitempty"` + // ReconcileFailedObjects specifies cleanup of failed objects + ReconcileFailedObjects *ObjectsCleanup `json:"reconcileFailedObjects,omitempty" yaml:"reconcileFailedObjects,omitempty"` +} + +// NewCleanup creates new cleanup +func NewCleanup() *Cleanup { + return new(Cleanup) +} + +// MergeFrom merges from specified cleanup +func (t *Cleanup) MergeFrom(from *Cleanup, _type MergeType) *Cleanup { + if from == nil { + return t + } + + if t == nil { + t = NewCleanup() + } + + switch _type { + case MergeTypeFillEmptyValues: + case MergeTypeOverrideByNonEmptyValues: + } + + t.UnknownObjects = t.UnknownObjects.MergeFrom(from.UnknownObjects, _type) + t.ReconcileFailedObjects = t.ReconcileFailedObjects.MergeFrom(from.ReconcileFailedObjects, _type) + + return t +} + +// GetUnknownObjects gets unknown objects cleanup +func (t *Cleanup) GetUnknownObjects() *ObjectsCleanup { + if t == nil { + return nil + } + return t.UnknownObjects +} + +// DefaultUnknownObjects makes default cleanup for known objects +func (t *Cleanup) DefaultUnknownObjects() *ObjectsCleanup { + return NewObjectsCleanup(). + SetStatefulSet(ObjectsCleanupDelete). + SetPVC(ObjectsCleanupDelete). + SetConfigMap(ObjectsCleanupDelete). + SetService(ObjectsCleanupDelete) +} + +// GetReconcileFailedObjects gets failed objects cleanup +func (t *Cleanup) GetReconcileFailedObjects() *ObjectsCleanup { + if t == nil { + return nil + } + return t.ReconcileFailedObjects +} + +// DefaultReconcileFailedObjects makes default cleanup for failed objects +func (t *Cleanup) DefaultReconcileFailedObjects() *ObjectsCleanup { + return NewObjectsCleanup(). + SetStatefulSet(ObjectsCleanupRetain). + SetPVC(ObjectsCleanupRetain). + SetConfigMap(ObjectsCleanupRetain). + SetService(ObjectsCleanupRetain) +} + +// SetDefaults set defaults for cleanup +func (t *Cleanup) SetDefaults() *Cleanup { + if t == nil { + return nil + } + t.UnknownObjects = t.DefaultUnknownObjects() + t.ReconcileFailedObjects = t.DefaultReconcileFailedObjects() + return t +} + +// Possible objects cleanup options +const ( + ObjectsCleanupUnspecified = "Unspecified" + ObjectsCleanupRetain = "Retain" + ObjectsCleanupDelete = "Delete" +) + +// ObjectsCleanup specifies object cleanup struct +type ObjectsCleanup struct { + StatefulSet string `json:"statefulSet,omitempty" yaml:"statefulSet,omitempty"` + PVC string `json:"pvc,omitempty" yaml:"pvc,omitempty"` + ConfigMap string `json:"configMap,omitempty" yaml:"configMap,omitempty"` + Service string `json:"service,omitempty" yaml:"service,omitempty"` + Secret string `json:"secret,omitempty" yaml:"secret,omitempty"` +} + +// NewObjectsCleanup creates new object cleanup +func NewObjectsCleanup() *ObjectsCleanup { + return new(ObjectsCleanup) +} + +// MergeFrom merges from specified cleanup +func (c *ObjectsCleanup) MergeFrom(from *ObjectsCleanup, _type MergeType) *ObjectsCleanup { + if from == nil { + return c + } + + if c == nil { + c = NewObjectsCleanup() + } + + switch _type { + case MergeTypeFillEmptyValues: + if c.StatefulSet == "" { + c.StatefulSet = from.StatefulSet + } + if c.PVC == "" { + c.PVC = from.PVC + } + if c.ConfigMap == "" { + c.ConfigMap = from.ConfigMap + } + if c.Service == "" { + c.Service = from.Service + } + if c.Secret == "" { + c.Secret = from.Secret + } + case MergeTypeOverrideByNonEmptyValues: + if from.StatefulSet != "" { + // Override by non-empty values only + c.StatefulSet = from.StatefulSet + } + if from.PVC != "" { + // Override by non-empty values only + c.PVC = from.PVC + } + if from.ConfigMap != "" { + // Override by non-empty values only + c.ConfigMap = from.ConfigMap + } + if from.Service != "" { + // Override by non-empty values only + c.Service = from.Service + } + if from.Secret != "" { + // Override by non-empty values only + c.Secret = from.Secret + } + } + + return c +} + +// GetStatefulSet gets stateful set +func (c *ObjectsCleanup) GetStatefulSet() string { + if c == nil { + return "" + } + return c.StatefulSet +} + +// SetStatefulSet sets stateful set +func (c *ObjectsCleanup) SetStatefulSet(v string) *ObjectsCleanup { + if c == nil { + return nil + } + c.StatefulSet = v + return c +} + +// GetPVC gets PVC +func (c *ObjectsCleanup) GetPVC() string { + if c == nil { + return "" + } + return c.PVC +} + +// SetPVC sets PVC +func (c *ObjectsCleanup) SetPVC(v string) *ObjectsCleanup { + if c == nil { + return nil + } + c.PVC = v + return c +} + +// GetConfigMap gets config map +func (c *ObjectsCleanup) GetConfigMap() string { + if c == nil { + return "" + } + return c.ConfigMap +} + +// SetConfigMap sets config map +func (c *ObjectsCleanup) SetConfigMap(v string) *ObjectsCleanup { + if c == nil { + return nil + } + c.ConfigMap = v + return c +} + +// GetService gets service +func (c *ObjectsCleanup) GetService() string { + if c == nil { + return "" + } + return c.Service +} + +// SetService sets service +func (c *ObjectsCleanup) SetService(v string) *ObjectsCleanup { + if c == nil { + return nil + } + c.Service = v + return c +} + +// GetSecret gets secret +func (c *ObjectsCleanup) GetSecret() string { + if c == nil { + return "" + } + return c.Secret +} + +// SetSecret sets service +func (c *ObjectsCleanup) SetSecret(v string) *ObjectsCleanup { + if c == nil { + return nil + } + c.Secret = v + return c +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_cluster.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_cluster.go new file mode 100644 index 00000000..a57930d6 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_cluster.go @@ -0,0 +1,476 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +// Cluster defines item of a clusters section of .configuration +type Cluster struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Zookeeper *ZookeeperConfig `json:"zookeeper,omitempty" yaml:"zookeeper,omitempty"` + Settings *Settings `json:"settings,omitempty" yaml:"settings,omitempty"` + Files *Settings `json:"files,omitempty" yaml:"files,omitempty"` + Templates *TemplatesList `json:"templates,omitempty" yaml:"templates,omitempty"` + SchemaPolicy *SchemaPolicy `json:"schemaPolicy,omitempty" yaml:"schemaPolicy,omitempty"` + Insecure *types.StringBool `json:"insecure,omitempty" yaml:"insecure,omitempty"` + Secure *types.StringBool `json:"secure,omitempty" yaml:"secure,omitempty"` + Secret *ClusterSecret `json:"secret,omitempty" yaml:"secret,omitempty"` + PDBManaged *types.StringBool `json:"pdbManaged,omitempty" yaml:"pdbManaged,omitempty"` + PDBMaxUnavailable *types.Int32 `json:"pdbMaxUnavailable,omitempty" yaml:"pdbMaxUnavailable,omitempty"` + Reconcile *ClusterReconcile `json:"reconcile,omitempty" yaml:"reconcile,omitempty"` + Layout *ChiClusterLayout `json:"layout,omitempty" yaml:"layout,omitempty"` + + Runtime ChiClusterRuntime `json:"-" yaml:"-"` +} + +type ChiClusterRuntime struct { + Address ChiClusterAddress `json:"-" yaml:"-"` + CHI *ClickHouseInstallation `json:"-" yaml:"-" testdiff:"ignore"` +} + +func (r *ChiClusterRuntime) GetAddress() IClusterAddress { + return &r.Address +} + +func (r ChiClusterRuntime) GetCR() ICustomResource { + return r.CHI +} + +func (r *ChiClusterRuntime) SetCR(cr ICustomResource) { + r.CHI = cr.(*ClickHouseInstallation) +} + +// ChiClusterAddress defines address of a cluster within ClickHouseInstallation +type ChiClusterAddress struct { + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + CHIName string `json:"chiName,omitempty" yaml:"chiName,omitempty"` + ClusterName string `json:"clusterName,omitempty" yaml:"clusterName,omitempty"` + ClusterIndex int `json:"clusterIndex,omitempty" yaml:"clusterIndex,omitempty"` +} + +func (a *ChiClusterAddress) GetNamespace() string { + return a.Namespace +} + +func (a *ChiClusterAddress) SetNamespace(namespace string) { + a.Namespace = namespace +} + +func (a *ChiClusterAddress) GetCRName() string { + return a.CHIName +} + +func (a *ChiClusterAddress) SetCRName(name string) { + a.CHIName = name +} + +func (a *ChiClusterAddress) GetClusterName() string { + return a.ClusterName +} + +func (a *ChiClusterAddress) SetClusterName(name string) { + a.ClusterName = name +} + +func (a *ChiClusterAddress) GetClusterIndex() int { + return a.ClusterIndex +} + +func (a *ChiClusterAddress) SetClusterIndex(index int) { + a.ClusterIndex = index +} + +func (cluster *Cluster) GetName() string { + return cluster.Name +} + +// HasName checks whether cluster has a name +func (cluster *Cluster) HasName() bool { + if cluster == nil { + return false + } + return len(cluster.GetName()) > 0 +} + +func (c *Cluster) GetZookeeper() *ZookeeperConfig { + return c.Zookeeper +} + +func (c *Cluster) GetSchemaPolicy() *SchemaPolicy { + return c.SchemaPolicy +} + +// GetInsecure is a getter +func (cluster *Cluster) GetInsecure() *types.StringBool { + if cluster == nil { + return nil + } + return cluster.Insecure +} + +// GetSecure is a getter +func (cluster *Cluster) GetSecure() *types.StringBool { + if cluster == nil { + return nil + } + return cluster.Secure +} + +// GetSecret is a getter +func (c *Cluster) GetSecret() *ClusterSecret { + return c.Secret +} + +// GetPDBManaged is a getter +func (cluster *Cluster) GetPDBManaged() *types.StringBool { + return cluster.PDBManaged +} + +// GetPDBMaxUnavailable is a getter +func (cluster *Cluster) GetPDBMaxUnavailable() *types.Int32 { + return cluster.PDBMaxUnavailable +} + +// GetReconcile is a getter +func (cluster *Cluster) GetReconcile() *ClusterReconcile { + cluster.Reconcile = cluster.Reconcile.Ensure() + return cluster.Reconcile +} + +// GetRuntime is a getter +func (cluster *Cluster) GetRuntime() IClusterRuntime { + return &cluster.Runtime +} + +// FillShardsReplicasExplicitlySpecified fills whether shard or replicas are explicitly specified +func (cluster *Cluster) FillShardsReplicasExplicitlySpecified() { + if len(cluster.Layout.Shards) > 0 { + cluster.Layout.ShardsExplicitlySpecified = true + } + if len(cluster.Layout.Replicas) > 0 { + cluster.Layout.ReplicasExplicitlySpecified = true + } +} + +// isShardExplicitlySpecified checks whether shard is explicitly specified +func (cluster *Cluster) isShardExplicitlySpecified() bool { + return cluster.Layout.ShardsExplicitlySpecified +} + +// isReplicaExplicitlySpecified checks whether replica is explicitly specified +func (cluster *Cluster) isReplicaExplicitlySpecified() bool { + return cluster.Layout.ReplicasExplicitlySpecified && !cluster.isShardExplicitlySpecified() +} + +// IsShardSpecified checks whether shard is explicitly specified +func (cluster *Cluster) isShardToBeUsedToInheritSettingsFrom() bool { + if !cluster.isShardExplicitlySpecified() && !cluster.isReplicaExplicitlySpecified() { + return true + } + + return cluster.isShardExplicitlySpecified() +} + +func (cluster *Cluster) SelectSettingsSourceFrom(shard IShard, replica IReplica) any { + if cluster.isShardToBeUsedToInheritSettingsFrom() { + return shard + } + return replica +} + +// InheritZookeeperFrom inherits zookeeper config from CHI +func (cluster *Cluster) InheritZookeeperFrom(chi *ClickHouseInstallation) { + if !cluster.Zookeeper.IsEmpty() { + // Has zk config explicitly specified alread + return + } + if chi.GetSpecT().Configuration == nil { + return + } + if chi.GetSpecT().Configuration.Zookeeper == nil { + return + } + + cluster.Zookeeper = cluster.Zookeeper.MergeFrom(chi.GetSpecT().Configuration.Zookeeper, MergeTypeFillEmptyValues) +} + +// InheritFilesFrom inherits files from CR +func (cluster *Cluster) InheritFilesFrom(chi *ClickHouseInstallation) { + if chi.GetSpecT().Configuration == nil { + return + } + if chi.GetSpecT().Configuration.Files == nil { + return + } + + // Propagate host section only + cluster.Files = cluster.Files.MergeFromCB(chi.GetSpecT().Configuration.Files, func(path string, _ *Setting) bool { + if section, err := GetSectionFromPath(path); err == nil { + if section.Equal(SectionHost) { + return true + } + } + + return false + }) +} + +// InheritClusterReconcileFrom inherits reconcile runtime from CHI +func (cluster *Cluster) InheritClusterReconcileFrom(chi *ClickHouseInstallation) { + if chi.Spec.Reconcile == nil { + return + } + reconcile := cluster.GetReconcile() + reconcile.Runtime = reconcile.Runtime.MergeFrom(chi.Spec.Reconcile.Runtime, MergeTypeFillEmptyValues) + reconcile.StatefulSet = reconcile.StatefulSet.MergeFrom(chi.Spec.Reconcile.StatefulSet) + reconcile.Host = reconcile.Host.MergeFrom(chi.Spec.Reconcile.Host) + cluster.Reconcile = reconcile +} + +// InheritTemplatesFrom inherits templates from CHI +func (cluster *Cluster) InheritTemplatesFrom(chi *ClickHouseInstallation) { + if chi.GetSpec().GetDefaults() == nil { + return + } + if chi.GetSpec().GetDefaults().Templates == nil { + return + } + cluster.Templates = cluster.Templates.MergeFrom(chi.GetSpec().GetDefaults().Templates, MergeTypeFillEmptyValues) + cluster.Templates.HandleDeprecatedFields() +} + +// GetServiceTemplate returns service template, if exists +func (cluster *Cluster) GetServiceTemplate() (*ServiceTemplate, bool) { + if !cluster.Templates.HasClusterServiceTemplate() { + return nil, false + } + name := cluster.Templates.GetClusterServiceTemplate() + return cluster.Runtime.CHI.GetServiceTemplate(name) +} + +// GetCR gets parent CR +func (cluster *Cluster) GetCR() *ClickHouseInstallation { + return cluster.Runtime.CHI +} + +func (cluster *Cluster) GetAncestor() ICluster { + return cluster.GetCR().GetAncestor().FindCluster(cluster.GetName()) +} + +// GetShard gets shard with specified index +func (cluster *Cluster) GetShard(shard int) *ChiShard { + return cluster.Layout.Shards[shard] +} + +// GetOrCreateHost gets or creates host on specified coordinates +func (cluster *Cluster) GetOrCreateHost(shard, replica int) *Host { + return cluster.Layout.HostsField.GetOrCreate(shard, replica) +} + +// GetReplica gets replica with specified index +func (cluster *Cluster) GetReplica(replica int) *ChiReplica { + return cluster.Layout.Replicas[replica] +} + +// FindShard finds shard by name or index. +// Expectations: name is expected to be a string, index is expected to be an int. +func (cluster *Cluster) FindShard(needle interface{}) IShard { + var resultShard *ChiShard + cluster.WalkShards(func(index int, shard IShard) error { + switch v := needle.(type) { + case string: + if shard.GetName() == v { + resultShard = shard.(*ChiShard) + } + case int: + if index == v { + resultShard = shard.(*ChiShard) + } + } + return nil + }) + return resultShard +} + +// FindHost finds host by name or index. +// Expectations: name is expected to be a string, index is expected to be an int. +func (cluster *Cluster) FindHost(needleShard interface{}, needleHost interface{}) *Host { + return cluster.FindShard(needleShard).FindHost(needleHost) +} + +// FirstHost finds first host in the cluster +func (cluster *Cluster) FirstHost() *Host { + var result *Host + cluster.WalkHosts(func(host *Host) error { + if result == nil { + result = host + } + return nil + }) + return result +} + +// WalkShards walks shards +func (cluster *Cluster) WalkShards(f func(index int, shard IShard) error) []error { + if cluster == nil { + return nil + } + res := make([]error, 0) + + for shardIndex := range cluster.Layout.Shards { + shard := cluster.Layout.Shards[shardIndex] + res = append(res, f(shardIndex, shard)) + } + + return res +} + +// WalkReplicas walks replicas +func (cluster *Cluster) WalkReplicas(f func(index int, replica *ChiReplica) error) []error { + res := make([]error, 0) + + for replicaIndex := range cluster.Layout.Replicas { + replica := cluster.Layout.Replicas[replicaIndex] + res = append(res, f(replicaIndex, replica)) + } + + return res +} + +// WalkHosts walks hosts +func (cluster *Cluster) WalkHosts(f func(host *Host) error) []error { + res := make([]error, 0) + + for shardIndex := range cluster.Layout.Shards { + shard := cluster.Layout.Shards[shardIndex] + for replicaIndex := range shard.Hosts { + host := shard.Hosts[replicaIndex] + res = append(res, f(host)) + } + } + + return res +} + +// WalkHostsByShards walks hosts by shards +func (cluster *Cluster) WalkHostsByShards(f func(shard, replica int, host *Host) error) []error { + + res := make([]error, 0) + + for shardIndex := range cluster.Layout.Shards { + shard := cluster.Layout.Shards[shardIndex] + for replicaIndex := range shard.Hosts { + host := shard.Hosts[replicaIndex] + res = append(res, f(shardIndex, replicaIndex, host)) + } + } + + return res +} + +func (cluster *Cluster) GetLayout() *ChiClusterLayout { + return cluster.Layout +} + +// WalkHostsByReplicas walks hosts by replicas +func (cluster *Cluster) WalkHostsByReplicas(f func(shard, replica int, host *Host) error) []error { + + res := make([]error, 0) + + for replicaIndex := range cluster.Layout.Replicas { + replica := cluster.Layout.Replicas[replicaIndex] + for shardIndex := range replica.Hosts { + host := replica.Hosts[shardIndex] + res = append(res, f(shardIndex, replicaIndex, host)) + } + } + + return res +} + +// HostsCount counts hosts +func (cluster *Cluster) HostsCount() int { + count := 0 + cluster.WalkHosts(func(host *Host) error { + count++ + return nil + }) + return count +} + +func (cluster *Cluster) IsZero() bool { + return cluster == nil +} + +func (cluster *Cluster) IsNonZero() bool { + return cluster != nil +} + +// IsStopped checks whether host is stopped +func (cluster *Cluster) IsStopped() bool { + return cluster.GetCR().IsStopped() +} + +func (cluster *Cluster) Ensure(create func() *Cluster) *Cluster { + if cluster == nil { + cluster = create() + } + return cluster +} + +// ChiClusterLayout defines layout section of .spec.configuration.clusters +type ChiClusterLayout struct { + ShardsCount int `json:"shardsCount,omitempty" yaml:"shardsCount,omitempty"` + ReplicasCount int `json:"replicasCount,omitempty" yaml:"replicasCount,omitempty"` + + // TODO refactor into map[string]ChiShard + Shards []*ChiShard `json:"shards,omitempty" yaml:"shards,omitempty"` + Replicas []*ChiReplica `json:"replicas,omitempty" yaml:"replicas,omitempty"` + + // Internal data + // Whether shards or replicas are explicitly specified as Shards []ChiShard or Replicas []ChiReplica + ShardsExplicitlySpecified bool `json:"-" yaml:"-" testdiff:"ignore"` + ReplicasExplicitlySpecified bool `json:"-" yaml:"-" testdiff:"ignore"` + HostsField *HostsField `json:"-" yaml:"-" testdiff:"ignore"` +} + +// NewChiClusterLayout creates new cluster layout +func NewChiClusterLayout() *ChiClusterLayout { + return new(ChiClusterLayout) +} + +func (l *ChiClusterLayout) GetReplicasCount() int { + return l.ReplicasCount +} + +func (l *ChiClusterLayout) Ensure() *ChiClusterLayout { + if l == nil { + l = NewChiClusterLayout() + } + return l +} + +// SchemaPolicy defines schema management policy - replica or shard-based +type SchemaPolicy struct { + Replica string `json:"replica" yaml:"replica"` + Shard string `json:"shard" yaml:"shard"` +} + +// NewClusterSchemaPolicy creates new cluster layout +func NewClusterSchemaPolicy() *SchemaPolicy { + return new(SchemaPolicy) +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_cluster_secret.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_cluster_secret.go new file mode 100644 index 00000000..2d3f5560 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_cluster_secret.go @@ -0,0 +1,99 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + core "k8s.io/api/core/v1" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +// ClusterSecret defines the shared secret for nodes to authenticate each other with +type ClusterSecret struct { + Auto *types.StringBool `json:"auto,omitempty" yaml:"auto,omitempty"` + Value string `json:"value,omitempty" yaml:"value,omitempty"` + ValueFrom *types.DataSource `json:"valueFrom,omitempty" yaml:"valueFrom,omitempty"` +} + +// ClusterSecretSourceName specifies name of the source where secret is provided +type ClusterSecretSourceName string + +// Possible values for ClusterSecretSourceName secret sources +const ( + ClusterSecretSourcePlaintext ClusterSecretSourceName = "plaintext" + ClusterSecretSourceSecretRef ClusterSecretSourceName = "secret_ref" + ClusterSecretSourceAuto ClusterSecretSourceName = "auto" + ClusterSecretSourceUnspecified ClusterSecretSourceName = "" +) + +// Source returns name of the source where secret is provided +func (s *ClusterSecret) Source() ClusterSecretSourceName { + if s == nil { + // No secret is specified at all + return ClusterSecretSourceUnspecified + } + + if s.HasValue() { + // Secret has explicit value specified + return ClusterSecretSourcePlaintext + } + + if s.HasSecretKeyRef() { + // Secret has SecretKeyRef specified + return ClusterSecretSourceSecretRef + } + + if s.Auto.IsTrue() { + // Secret is auto-generated + return ClusterSecretSourceAuto + } + + // No secret is specified at all + return ClusterSecretSourceUnspecified +} + +// HasValue checks whether explicit plaintext value is specified +func (s *ClusterSecret) HasValue() bool { + if s == nil { + return false + } + return s.Value != "" +} + +// GetSecretKeyRef gets SecretKeySelector (typically named as SecretKeyRef) or nil +func (s *ClusterSecret) GetSecretKeyRef() *core.SecretKeySelector { + if s == nil { + return nil + } + if s.ValueFrom == nil { + return nil + } + return s.ValueFrom.SecretKeyRef +} + +// HasSecretKeyRef checks whether SecretKeySelector (typically named as SecretKeyRef) is available +func (s *ClusterSecret) HasSecretKeyRef() bool { + return s.GetSecretKeyRef() != nil +} + +// GetAutoSecretKeyRef gets SecretKeySelector (typically named as SecretKeyRef) of an auto-generated secret or nil +func (s *ClusterSecret) GetAutoSecretKeyRef(name string) *core.SecretKeySelector { + return &core.SecretKeySelector{ + LocalObjectReference: core.LocalObjectReference{ + Name: name, + }, + Key: "secret", + } +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_configuration_chi.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_configuration_chi.go new file mode 100644 index 00000000..013094c5 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_configuration_chi.go @@ -0,0 +1,123 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +const ( + // CommonConfigDirClickHouse specifies folder's name, where generated common XML files for ClickHouse would be placed + CommonConfigDirClickHouse = "config.d" + + // UsersConfigDirClickHouse specifies folder's name, where generated users XML files for ClickHouse would be placed + UsersConfigDirClickHouse = "users.d" + + // HostConfigDirClickHouse specifies folder's name, where generated host XML files for ClickHouse would be placed + HostConfigDirClickHouse = "conf.d" + + // TemplatesDirClickHouse specifies folder's name where ClickHouseInstallationTemplates are located + TemplatesDirClickHouse = "templates.d" +) + +const ( + // CommonConfigDirKeeper specifies folder's name, where generated common XML files for ClickHouse would be placed + CommonConfigDirKeeper = "keeper_config.d" + + // UsersConfigDirKeeper specifies folder's name, where generated users XML files for ClickHouse would be placed + UsersConfigDirKeeper = "users.d" + + // HostConfigDirKeeper specifies folder's name, where generated host XML files for ClickHouse would be placed + HostConfigDirKeeper = "conf.d" + + // TemplatesDirKeeper specifies folder's name where ClickHouseInstallationTemplates are located + TemplatesDirKeeper = "templates.d" +) + +// Configuration defines configuration section of .spec +type Configuration struct { + Zookeeper *ZookeeperConfig `json:"zookeeper,omitempty" yaml:"zookeeper,omitempty"` + Users *Settings `json:"users,omitempty" yaml:"users,omitempty"` + Profiles *Settings `json:"profiles,omitempty" yaml:"profiles,omitempty"` + Quotas *Settings `json:"quotas,omitempty" yaml:"quotas,omitempty"` + Settings *Settings `json:"settings,omitempty" yaml:"settings,omitempty"` + Files *Settings `json:"files,omitempty" yaml:"files,omitempty"` + Clusters []*Cluster `json:"clusters,omitempty" yaml:"clusters,omitempty"` +} + +// NewConfiguration creates new Configuration objects +func NewConfiguration() *Configuration { + return new(Configuration) +} + +func (c *Configuration) Ensure() *Configuration { + if c == nil { + c = NewConfiguration() + } + return c +} + +func (c *Configuration) GetUsers() *Settings { + if c == nil { + return nil + } + return c.Users +} + +func (c *Configuration) GetProfiles() *Settings { + if c == nil { + return nil + } + return c.Profiles +} + +func (c *Configuration) GetQuotas() *Settings { + if c == nil { + return nil + } + return c.Quotas +} + +func (c *Configuration) GetSettings() *Settings { + if c == nil { + return nil + } + return c.Settings +} + +func (c *Configuration) GetFiles() *Settings { + if c == nil { + return nil + } + return c.Files +} + +// MergeFrom merges from specified source +func (c *Configuration) MergeFrom(from *Configuration, _type MergeType) *Configuration { + if from == nil { + return c + } + + c = c.Ensure() + + c.Zookeeper = c.Zookeeper.MergeFrom(from.Zookeeper, _type) + c.Users = c.Users.MergeFrom(from.Users) + c.Profiles = c.Profiles.MergeFrom(from.Profiles) + c.Quotas = c.Quotas.MergeFrom(from.Quotas) + c.Settings = c.Settings.MergeFrom(from.Settings) + c.Files = c.Files.MergeFrom(from.Files) + + // TODO merge clusters + // Copy Clusters for now + c.Clusters = from.Clusters + + return c +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_configuration_chop.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_configuration_chop.go new file mode 100644 index 00000000..0ac0474d --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_configuration_chop.go @@ -0,0 +1,1607 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "encoding/json" + "fmt" + "os" + "regexp" + "sort" + "strconv" + "strings" + "sync" + "time" + + log "github.com/golang/glog" + "github.com/imdario/mergo" + "gopkg.in/yaml.v3" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/deployment" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" +) + +const ( + // Default values for update timeout and polling period in seconds + defaultStatefulSetUpdateTimeout = 300 + defaultStatefulSetUpdatePollInterval = 5 + + // Default values for ClickHouse user configuration + // 1. user/profile + // 2. user/quota + // 3. user/networks/ip + // 4. user/password + defaultChConfigUserDefaultProfile = "default" + defaultChConfigUserDefaultQuota = "default" + defaultChConfigUserDefaultNetworkIP = "::/0" + defaultChConfigUserDefaultPassword = "default" + + // Possible values for ClickHouse scheme + + // ChSchemeHTTP specifies HTTP access scheme + ChSchemeHTTP = "http" + // ChSchemeHTTPS specifies HTTPS access scheme + ChSchemeHTTPS = "https" + // ChSchemeAuto specifies that operator has to decide itself should https or http be used + ChSchemeAuto = "auto" + + // Username and Password to be used by operator to connect to ClickHouse instances for + // 1. Metrics requests + // 2. Schema maintenance + // User credentials can be specified in additional ClickHouse config files located in `chUsersConfigsPath` folder + defaultChScheme = ChSchemeAuto + defaultChUsername = "clickhouse_operator" + defaultChPassword = "clickhouse_operator_password" + defaultChPort = 8123 + defaultChRootCA = "" + + // Timeouts used to limit connection and queries from the operator to ClickHouse instances. In seconds + // defaultTimeoutConnect specifies default timeout to connect to the ClickHouse instance. In seconds + defaultTimeoutConnect = 5 + // defaultTimeoutQuery specifies default timeout to query the CLickHouse instance. In seconds + defaultTimeoutQuery = 5 + // defaultTimeoutCollect specifies default timeout to collect metrics from the ClickHouse instance. In seconds + defaultTimeoutCollect = 8 + + // defaultMetricsTablesRegexp specifies default regexp to match tables in system database to fetch metrics from + defaultMetricsTablesRegexp = "^(metrics|custom_metrics)$" + + // defaultReconcileCHIsThreadsNumber specifies default number of controller threads running concurrently. + // Used in case no other specified in config + defaultReconcileCHIsThreadsNumber = 1 + + // defaultReconcileShardsThreadsNumber specifies the default number of threads usable for concurrent shard reconciliation + // within a single cluster reconciliation. Defaults to 1, which means strictly sequential shard reconciliation. + defaultReconcileShardsThreadsNumber = 1 + + // defaultReconcileShardsMaxConcurrencyPercent specifies the maximum integer percentage of shards that may be reconciled + // concurrently during cluster reconciliation. This counterbalances the fact that this is an operator setting, + // that different clusters will have different shard counts, and that the shard concurrency capacity is specified + // above in terms of a number of threads to use (up to). Example: overriding to 100 means all shards may be + // reconciled concurrently, if the number of shard reconciliation threads is greater than or equal to the number + // of shards in the cluster. + defaultReconcileShardsMaxConcurrencyPercent = 50 + + // DefaultReconcileThreadsWarmup specifies default reconcile threads warmup time + DefaultReconcileThreadsWarmup = 10 * time.Second + + // DefaultReconcileSystemThreadsNumber specifies default number of system controller threads running concurrently. + // Used in case no other specified in config + DefaultReconcileSystemThreadsNumber = 1 + + // defaultTerminationGracePeriod specifies default value for TerminationGracePeriod + defaultTerminationGracePeriod = 30 + // defaultRevisionHistoryLimit specifies default value for RevisionHistoryLimit + defaultRevisionHistoryLimit = 10 +) + +// Username/password replacers +const ( + UsernameReplacer = "***" + PasswordReplacer = "***" +) + +const ( + // What to do in case StatefulSet can't reach new Generation - abort CHI reconcile + OnStatefulSetCreateFailureActionAbort = "abort" + + // What to do in case StatefulSet can't reach new Generation - delete newly created problematic StatefulSet + OnStatefulSetCreateFailureActionDelete = "delete" + + // What to do in case StatefulSet can't reach new Generation - do nothing, keep StatefulSet broken and move to the next + OnStatefulSetCreateFailureActionIgnore = "ignore" +) + +const ( + // What to do in case StatefulSet can't reach new Generation - abort CHI reconcile + OnStatefulSetUpdateFailureActionAbort = "abort" + + // What to do in case StatefulSet can't reach new Generation - delete Pod and rollback StatefulSet to previous Generation + // Pod would be recreated by StatefulSet based on rollback-ed configuration + OnStatefulSetUpdateFailureActionRollback = "rollback" + + // What to do in case StatefulSet can't reach new Generation - do nothing, keep StatefulSet broken and move to the next + OnStatefulSetUpdateFailureActionIgnore = "ignore" +) + +const ( + // What to do in case StatefulSet needs to be recreated due to PVC data loss or missing volumes + // Abort - Loss: abort CHI reconcile + OnStatefulSetRecreateOnDataLossActionAbort = "abort" + + // Recreate - Loss: proceed and recreate StatefulSet + OnStatefulSetRecreateOnDataLossActionRecreate = "recreate" + + // What to do in case StatefulSet needs to be recreated due to update failure or StatefulSet not ready + // Abort - Failure: abort CHI reconcile + OnStatefulSetRecreateOnUpdateFailureActionAbort = "abort" + + // Recreate - Failure: proceed and recreate StatefulSet + OnStatefulSetRecreateOnUpdateFailureActionRecreate = "recreate" +) + +const ( + defaultMaxReplicationDelay = 10 +) + +// OperatorConfig specifies operator configuration +// !!! IMPORTANT !!! +// !!! IMPORTANT !!! +// !!! IMPORTANT !!! +// !!! IMPORTANT !!! +// !!! IMPORTANT !!! +// Do not forget to update func (config *OperatorConfig) String() +// Do not forget to update CRD spec + +// OperatorConfigRuntime specifies runtime config +type OperatorConfigRuntime struct { + // Full path to the config file and folder where file part of this OperatorConfig originates from + ConfigFilePath string `json:"configFilePath" yaml:"configFilePath"` + ConfigFolderPath string `json:"configFolderPath" yaml:"configFolderPath"` + // Namespace and Name of the config Custom Resource + ConfigCRNamespace string `json:"configCRNamespace" yaml:"configCRNamespace"` + ConfigCRName string `json:"configCRName" yaml:"configCRName"` + + // ConfigCRSources specifies list of Custom Resource-based configuration sources + ConfigCRSources []ConfigCRSource `json:"configCRSources" yaml:"configCRSources"` + + // Namespace specifies namespace where the operator runs + Namespace string `json:"namespace" yaml:"namespace"` +} + +// OperatorConfigWatch specifies watch section +type OperatorConfigWatch struct { + // Namespaces where operator watches for events + Namespaces OperatorConfigWatchNamespaces `json:"namespaces" yaml:"namespaces"` +} + +type OperatorConfigWatchNamespaces struct { + Include *types.Strings `json:"include" yaml:"include"` + Exclude *types.Strings `json:"exclude" yaml:"exclude"` +} + +func (n *OperatorConfigWatchNamespaces) UnmarshalJSON(data []byte) error { + type OperatorConfigWatchNamespaces2 OperatorConfigWatchNamespaces + var namespaces OperatorConfigWatchNamespaces2 + if err := json.Unmarshal(data, &namespaces); err == nil { + s := OperatorConfigWatchNamespaces{ + Include: namespaces.Include, + Exclude: namespaces.Exclude, + } + *n = s + return nil + } + + var sl []string + if err := json.Unmarshal(data, &sl); err == nil { + s := OperatorConfigWatchNamespaces{ + Include: types.NewStrings(sl), + } + *n = s + return nil + } + + return fmt.Errorf("unable to OperatorConfigWatchNamespaces.UnmarshalJSON()") +} + +// OperatorConfigConfig specifies Config section +type OperatorConfigConfig struct { + File OperatorConfigFile `json:"file" yaml:"file"` + + User OperatorConfigUser `json:"user" yaml:"user"` + + Network struct { + HostRegexpTemplate string `json:"hostRegexpTemplate" yaml:"hostRegexpTemplate"` + } `json:"network" yaml:"network"` +} + +// OperatorConfigRestartPolicyRuleSet specifies set of rules +type OperatorConfigRestartPolicyRuleSet map[types.Matchable]types.StringBool + +// OperatorConfigRestartPolicyRule specifies ClickHouse version and rules for this version +type OperatorConfigRestartPolicyRule struct { + Version string `json:"version" yaml:"version"` + Rules []OperatorConfigRestartPolicyRuleSet `json:"rules" yaml:"rules"` +} + +// OperatorConfigRestartPolicy specifies operator's configuration changes restart policy +type OperatorConfigRestartPolicy struct { + Rules []OperatorConfigRestartPolicyRule `json:"rules" yaml:"rules"` +} + +// OperatorConfigAddonRule specifies ClickHouse version and rules for this version +type OperatorConfigAddonRule struct { + Version string `json:"version,omitempty" yaml:"version,omitempty"` + Spec *AddonSpec `json:"spec,omitempty" yaml:"spec,omitempty"` +} + +type AddonSpec struct { + Configuration *AddonConfiguration `json:"configuration,omitempty" yaml:"configuration,omitempty"` +} + +type AddonConfiguration struct { + Users map[string]string `json:"users,omitempty" yaml:"users,omitempty"` + Profiles map[string]string `json:"profiles,omitempty" yaml:"profiles,omitempty"` + Quotas map[string]string `json:"quotas,omitempty" yaml:"quotas,omitempty"` + Settings map[string]string `json:"settings,omitempty" yaml:"settings,omitempty"` + Files map[string]string `json:"files,omitempty" yaml:"files,omitempty"` +} + +// OperatorConfigRestartPolicy specifies operator's configuration changes restart policy +type OperatorConfigAddons struct { + Rules []OperatorConfigAddonRule `json:"rules" yaml:"rules"` +} + +// OperatorConfigFile specifies File section +type OperatorConfigFile struct { + Path struct { + // Paths where to look for additional ClickHouse config .xml files to be mounted into Pod + Common string `json:"common" yaml:"common"` + Host string `json:"host" yaml:"host"` + User string `json:"user" yaml:"user"` + } `json:"path" yaml:"path"` + + Runtime OperatorConfigFileRuntime `json:"-" yaml:"-"` +} + +// OperatorConfigFileRuntime specifies runtime section +type OperatorConfigFileRuntime struct { + // OperatorConfig files fetched from paths specified above. Maps "file name->file content" + CommonConfigFiles map[string]string `json:"-" yaml:"-"` + HostConfigFiles map[string]string `json:"-" yaml:"-"` + UsersConfigFiles map[string]string `json:"-" yaml:"-"` +} + +type IOperatorConfigFilesPathsGetter interface { + GetCommonConfigFiles() map[string]string + GetHostConfigFiles() map[string]string + GetUsersConfigFiles() map[string]string +} + +func (r OperatorConfigFileRuntime) GetCommonConfigFiles() map[string]string { + return r.CommonConfigFiles +} + +func (r OperatorConfigFileRuntime) GetHostConfigFiles() map[string]string { + return r.HostConfigFiles +} + +func (r OperatorConfigFileRuntime) GetUsersConfigFiles() map[string]string { + return r.UsersConfigFiles +} + +// OperatorConfigUser specifies User section +type OperatorConfigUser struct { + Default OperatorConfigDefault `json:"default" yaml:"default"` +} + +// OperatorConfigDefault specifies user-default section +type OperatorConfigDefault struct { + // Default values for ClickHouse user configuration + // 1. user/profile - string + // 2. user/quota - string + // 3. user/networks/ip - multiple strings + // 4. user/password - string + Profile string `json:"profile" yaml:"profile"` + Quota string `json:"quota" yaml:"quota"` + NetworksIP []string `json:"networksIP" yaml:"networksIP"` + Password string `json:"password" yaml:"password"` +} + +// type RestartPolicy map[Matchable]StringBool + +// OperatorConfigClickHouse specifies ClickHouse section +type OperatorConfigClickHouse struct { + Config OperatorConfigConfig `json:"configuration" yaml:"configuration"` + ConfigRestartPolicy OperatorConfigRestartPolicy `json:"configurationRestartPolicy" yaml:"configurationRestartPolicy"` + + Access struct { + // Username and Password to be used by operator to connect to ClickHouse instances + // for + // 1. Metrics requests + // 2. Schema maintenance + // User credentials can be specified in additional ClickHouse config files located in `chUsersConfigsPath` folder + Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` + Username string `json:"username,omitempty" yaml:"username,omitempty"` + Password string `json:"password,omitempty" yaml:"password,omitempty"` + RootCA string `json:"rootCA,omitempty" yaml:"rootCA,omitempty"` + + // Location of k8s Secret with username and password to be used by the operator to connect to ClickHouse instances + // Can be used instead of explicitly specified (above) username and password + Secret struct { + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + + Runtime struct { + // Username and Password to be used by operator to connect to ClickHouse instances + // extracted from k8s secret specified above. + Username string + Password string + Fetched bool + Error string + } + } `json:"secret" yaml:"secret"` + + // Port where to connect to ClickHouse instances to + Port int `json:"port" yaml:"port"` + + // Timeouts used to limit connection and queries from the operator to ClickHouse instances + Timeouts struct { + Connect time.Duration `json:"connect" yaml:"connect"` + Query time.Duration `json:"query" yaml:"query"` + } `json:"timeouts" yaml:"timeouts"` + } `json:"access" yaml:"access"` + + Addons OperatorConfigAddons `json:"addons" yaml:"addons"` + + // Metrics used to specify how the operator fetches metrics from ClickHouse instances + Metrics struct { + Timeouts struct { + Collect time.Duration `json:"collect" yaml:"collect"` + } `json:"timeouts" yaml:"timeouts"` + // TablesRegexp specifies regexp to match tables in system database to fetch metrics from. + // Multiple tables can be matched using regexp. Matched tables are merged using merge() table function. + // Default is "^(metrics|custom_metrics)$" which fetches from both system.metrics and system.custom_metrics. + TablesRegexp string `json:"tablesRegexp" yaml:"tablesRegexp"` + } `json:"metrics" yaml:"metrics"` +} + +// OperatorConfigKeeper specifies Keeper section +type OperatorConfigKeeper struct { + Config OperatorConfigConfig `json:"configuration" yaml:"configuration"` +} + +// OperatorConfigTemplate specifies template section +type OperatorConfigTemplate struct { + CHI OperatorConfigCHI `json:"chi" yaml:"chi"` +} + +// OperatorConfigCHIPolicy specifies string value of .template.chi.policy +type OperatorConfigCHIPolicy string + +// String is a stringifier +func (p OperatorConfigCHIPolicy) String() string { + return string(p) +} + +// ToLower provides the same functionality as strings.ToLower() +func (p OperatorConfigCHIPolicy) ToLower() string { + return strings.ToLower(p.String()) +} + +// Equals checks whether OperatorConfigCHIPolicy is equal to another one +func (p OperatorConfigCHIPolicy) Equals(another OperatorConfigCHIPolicy) bool { + return p.ToLower() == another.ToLower() +} + +// Possible values for OperatorConfigCHIPolicy +const ( + OperatorConfigCHIPolicyReadOnStart OperatorConfigCHIPolicy = "ReadOnStart" + OperatorConfigCHIPolicyApplyOnNextReconcile OperatorConfigCHIPolicy = "ApplyOnNextReconcile" + defaultOperatorConfigCHIPolicy OperatorConfigCHIPolicy = OperatorConfigCHIPolicyApplyOnNextReconcile +) + +// OperatorConfigCHI specifies template CHI section +type OperatorConfigCHI struct { + // Policy specifies how to handle CHITs + Policy OperatorConfigCHIPolicy `json:"policy" yaml:"policy"` + // Path where to look for ClickHouseInstallation templates .yaml files + Path string `json:"path" yaml:"path"` + + Runtime OperatorConfigCHIRuntime `json:"runtime,omitempty" yaml:"runtime,omitempty"` +} + +// OperatorConfigCHIRuntime specifies chi runtime section +type OperatorConfigCHIRuntime struct { + // CHI template files fetched from the path specified above. Maps "file name->file content" + TemplateFiles map[string]string `json:"templateFiles,omitempty" yaml:"templateFiles,omitempty"` + // CHI template objects unmarshalled from CHITemplateFiles. Maps "metadata.name->object" + Templates []*ClickHouseInstallation `json:"-" yaml:"-"` + mutex sync.RWMutex `json:"-" yaml:"-"` +} + +// OperatorConfigReconcile specifies reconcile section +type OperatorConfigReconcile struct { + Runtime OperatorConfigReconcileRuntime `json:"runtime" yaml:"runtime"` + + StatefulSet struct { + Create struct { + OnFailure string `json:"onFailure" yaml:"onFailure"` + } `json:"create" yaml:"create"` + + Update struct { + Timeout uint64 `json:"timeout" yaml:"timeout"` + PollInterval uint64 `json:"pollInterval" yaml:"pollInterval"` + OnFailure string `json:"onFailure" yaml:"onFailure"` + } `json:"update" yaml:"update"` + + Recreate struct { + OnDataLoss string `json:"onDataLoss" yaml:"onDataLoss"` + OnUpdateFailure string `json:"onUpdateFailure" yaml:"onUpdateFailure"` + } `json:"recreate" yaml:"recreate"` + } `json:"statefulSet" yaml:"statefulSet"` + + Host ReconcileHost `json:"host" yaml:"host"` +} + +type OperatorConfigReconcileRuntime struct { + ReconcileCHIsThreadsNumber int `json:"reconcileCHIsThreadsNumber" yaml:"reconcileCHIsThreadsNumber"` + ReconcileShardsThreadsNumber int `json:"reconcileShardsThreadsNumber" yaml:"reconcileShardsThreadsNumber"` + ReconcileShardsMaxConcurrencyPercent int `json:"reconcileShardsMaxConcurrencyPercent" yaml:"reconcileShardsMaxConcurrencyPercent"` + + // DEPRECATED, is replaced with reconcileCHIsThreadsNumber + ThreadsNumber int `json:"threadsNumber" yaml:"threadsNumber"` +} + +// ReconcileHost defines reconcile host config +type ReconcileHost struct { + Wait ReconcileHostWait `json:"wait" yaml:"wait"` + Drop ReconcileHostDrop `json:"drop" yaml:"drop"` +} + +func (rh ReconcileHost) Normalize() ReconcileHost { + rh.Wait = rh.Wait.Normalize() + rh.Drop = rh.Drop.Normalize() + return rh +} + +func (rh ReconcileHost) MergeFrom(from ReconcileHost) ReconcileHost { + rh.Wait = rh.Wait.MergeFrom(from.Wait) + rh.Drop = rh.Drop.MergeFrom(from.Drop) + return rh +} + +// ReconcileHostWait defines reconcile host wait config +type ReconcileHostWait struct { + Exclude *types.StringBool `json:"exclude,omitempty" yaml:"exclude,omitempty"` + Queries *types.StringBool `json:"queries,omitempty" yaml:"queries,omitempty"` + Include *types.StringBool `json:"include,omitempty" yaml:"include,omitempty"` + Replicas *ReconcileHostWaitReplicas `json:"replicas,omitempty" yaml:"replicas,omitempty"` + Probes *ReconcileHostWaitProbes `json:"probes,omitempty" yaml:"probes,omitempty"` +} + +func (wait ReconcileHostWait) Normalize() ReconcileHostWait { + if wait.Replicas == nil { + wait.Replicas = &ReconcileHostWaitReplicas{} + } + + if wait.Replicas.Delay == nil { + // Default update timeout in seconds + wait.Replicas.Delay = types.NewInt32(defaultMaxReplicationDelay) + } + + if wait.Probes == nil { + wait.Probes = &ReconcileHostWaitProbes{} + } + + return wait +} + +func (wait ReconcileHostWait) MergeFrom(from ReconcileHostWait) ReconcileHostWait { + wait.Exclude = wait.Exclude.MergeFrom(from.Exclude) + wait.Queries = wait.Queries.MergeFrom(from.Queries) + wait.Include = wait.Include.MergeFrom(from.Include) + wait.Replicas = wait.Replicas.MergeFrom(from.Replicas) + wait.Probes = wait.Probes.MergeFrom(from.Probes) + + return wait +} + +// ReconcileHostDrop defines reconcile host drop config +type ReconcileHostDrop struct { + Replicas *ReconcileHostDropReplicas `json:"replicas,omitempty" yaml:"replicas,omitempty"` +} + +func (drop ReconcileHostDrop) Normalize() ReconcileHostDrop { + if drop.Replicas == nil { + drop.Replicas = &ReconcileHostDropReplicas{} + } + + return drop +} + +func (drop ReconcileHostDrop) MergeFrom(from ReconcileHostDrop) ReconcileHostDrop { + drop.Replicas = drop.Replicas.MergeFrom(from.Replicas) + + return drop +} + +type ReconcileHostWaitReplicas struct { + All *types.StringBool `json:"all,omitempty" yaml:"all,omitempty"` + New *types.StringBool `json:"new,omitempty" yaml:"new,omitempty"` + Delay *types.Int32 `json:"delay,omitempty" yaml:"delay,omitempty"` +} + +func (r *ReconcileHostWaitReplicas) MergeFrom(from *ReconcileHostWaitReplicas) *ReconcileHostWaitReplicas { + if from == nil { + // Nothing to merge from, keep original value + return r + } + + // From now on we have `from` specified + + if r == nil { + // Recipient is not specified, just use `from` value + return from + } + + // Both recipient and `from` are specified, need to walk over fields + + r.All = r.All.MergeFrom(from.All) + r.New = r.New.MergeFrom(from.New) + r.Delay = r.Delay.MergeFrom(from.Delay) + + return r +} + +type ReconcileHostWaitProbes struct { + Startup *types.StringBool `json:"startup,omitempty" yaml:"startup,omitempty"` + Readiness *types.StringBool `json:"readiness,omitempty" yaml:"readiness,omitempty"` +} + +func (p *ReconcileHostWaitProbes) GetStartup() *types.StringBool { + if p == nil { + return nil + } + return p.Startup +} + +func (p *ReconcileHostWaitProbes) GetReadiness() *types.StringBool { + if p == nil { + return nil + } + return p.Readiness +} + +func (p *ReconcileHostWaitProbes) MergeFrom(from *ReconcileHostWaitProbes) *ReconcileHostWaitProbes { + if from == nil { + // Nothing to merge from, keep original value + return p + } + + // From now on we have `from` specified + + if p == nil { + // Recipient is not specified, just use `from` value + return from + } + + // Both recipient and `from` are specified, need to walk over fields + + p.Startup = p.Startup.MergeFrom(from.Startup) + p.Readiness = p.Readiness.MergeFrom(from.Readiness) + + return p +} + +type ReconcileHostDropReplicas struct { + OnDelete *types.StringBool `json:"onDelete,omitempty" yaml:"onDelete,omitempty"` + OnLostVolume *types.StringBool `json:"onLostVolume,omitempty" yaml:"onLostVolume,omitempty"` + Active *types.StringBool `json:"active,omitempty" yaml:"active,omitempty"` +} + +func (r *ReconcileHostDropReplicas) MergeFrom(from *ReconcileHostDropReplicas) *ReconcileHostDropReplicas { + if from == nil { + // Nothing to merge from, keep original value + return r + } + + // From now on we have `from` specified + + if r == nil { + // Recipient is not specified, just use `from` value + return from + } + + // Both recipient and `from` are specified, need to walk over fields + + r.OnDelete = r.OnDelete.MergeFrom(from.OnDelete) + r.OnLostVolume = r.OnLostVolume.MergeFrom(from.OnLostVolume) + r.Active = r.Active.MergeFrom(from.Active) + + return r +} + +// OperatorConfigAnnotation specifies annotation section +type OperatorConfigAnnotation struct { + // When transferring annotations from the chi/chit.metadata to CHI objects, use these filters. + Include []string `json:"include" yaml:"include"` + Exclude []string `json:"exclude" yaml:"exclude"` +} + +// OperatorConfigLabel specifies label section +type OperatorConfigLabel struct { + // When transferring labels from the chi/chit.metadata to child objects, use these filters. + Include []string `json:"include" yaml:"include"` + Exclude []string `json:"exclude" yaml:"exclude"` + + // Whether to append *Scope* labels to StatefulSet and Pod. + AppendScopeString types.StringBool `json:"appendScope" yaml:"appendScope"` + + Runtime OperatorConfigLabelRuntime `json:"runtime" yaml:"runtime"` +} + +type OperatorConfigLabelRuntime struct { + AppendScope bool `json:"appendScope" yaml:"appendScope"` +} + +type OperatorConfigMetrics struct { + Labels OperatorConfigMetricsLabels `json:"labels" yaml:"labels"` +} + +type OperatorConfigMetricsLabels struct { + Exclude []string `json:"exclude" yaml:"exclude"` +} + +type OperatorConfigStatus struct { + Fields OperatorConfigStatusFields `json:"fields" yaml:"fields"` +} + +type OperatorConfigStatusFields struct { + Action *types.StringBool `json:"action,omitempty" yaml:"action,omitempty"` + Actions *types.StringBool `json:"actions,omitempty" yaml:"actions,omitempty"` + Error *types.StringBool `json:"error,omitempty" yaml:"error,omitempty"` + Errors *types.StringBool `json:"errors,omitempty" yaml:"errors,omitempty"` +} + +type ConfigCRSource struct { + Namespace string + Name string +} + +// OperatorConfig specifies operator config +type OperatorConfig struct { + Runtime OperatorConfigRuntime `json:"runtime" yaml:"runtime"` + Watch OperatorConfigWatch `json:"watch" yaml:"watch"` + ClickHouse OperatorConfigClickHouse `json:"clickhouse" yaml:"clickhouse"` + Keeper OperatorConfigKeeper `json:"keeper" yaml:"keeper"` + Template OperatorConfigTemplate `json:"template" yaml:"template"` + Reconcile OperatorConfigReconcile `json:"reconcile" yaml:"reconcile"` + Annotation OperatorConfigAnnotation `json:"annotation" yaml:"annotation"` + Label OperatorConfigLabel `json:"label" yaml:"label"` + Metrics OperatorConfigMetrics `json:"metrics" yaml:"metrics"` + Status OperatorConfigStatus `json:"status" yaml:"status"` + StatefulSet struct { + // Revision history limit + RevisionHistoryLimit int `json:"revisionHistoryLimit" yaml:"revisionHistoryLimit"` + } `json:"statefulSet" yaml:"statefulSet"` + Pod struct { + // Grace period for Pod termination. + TerminationGracePeriod int `json:"terminationGracePeriod" yaml:"terminationGracePeriod"` + } `json:"pod" yaml:"pod"` + Logger struct { + // Logger section + LogToStderr string `json:"logtostderr" yaml:"logtostderr"` + AlsoLogToStderr string `json:"alsologtostderr" yaml:"alsologtostderr"` + V string `json:"v" yaml:"v"` + StderrThreshold string `json:"stderrthreshold" yaml:"stderrthreshold"` + VModule string `json:"vmodule" yaml:"vmodule"` + LogBacktraceAt string `json:"log_backtrace_at" yaml:"log_backtrace_at"` + } `json:"logger" yaml:"logger"` + + // + // The end of OperatorConfig + // + // !!! IMPORTANT !!! + // !!! IMPORTANT !!! + // !!! IMPORTANT !!! + // !!! IMPORTANT !!! + // !!! IMPORTANT !!! + // Do not forget to update func (config *OperatorConfig) String() + // Do not forget to update CRD spec + + //////////////////// + // DEPRECATED! + // TO BE REMOVED! + //////////////////// + + // WatchNamespaces where operator watches for events + WatchNamespaces []string `json:"watchNamespaces" yaml:"watchNamespaces"` + // Paths where to look for additional ClickHouse config .xml files to be mounted into Pod + CHCommonConfigsPath string `json:"chCommonConfigsPath" yaml:"chCommonConfigsPath"` + CHHostConfigsPath string `json:"chHostConfigsPath" yaml:"chHostConfigsPath"` + CHUsersConfigsPath string `json:"chUsersConfigsPath" yaml:"chUsersConfigsPath"` + + // Path where to look for ClickHouseInstallation templates .yaml files + CHITemplatesPath string `json:"chiTemplatesPath" yaml:"chiTemplatesPath"` + // Create/Update StatefulSet behavior - for how long to wait for StatefulSet to reach new Generation + StatefulSetUpdateTimeout uint64 `json:"statefulSetUpdateTimeout" yaml:"statefulSetUpdateTimeout"` + // Create/Update StatefulSet behavior - for how long to sleep while polling StatefulSet to reach new Generation + StatefulSetUpdatePollPeriod uint64 `json:"statefulSetUpdatePollPeriod" yaml:"statefulSetUpdatePollPeriod"` + + // Rolling Create/Update behavior + // StatefulSet create behavior - what to do in case StatefulSet can't reach new Generation + OnStatefulSetCreateFailureAction string `json:"onStatefulSetCreateFailureAction" yaml:"onStatefulSetCreateFailureAction"` + // StatefulSet update behavior - what to do in case StatefulSet can't reach new Generation + OnStatefulSetUpdateFailureAction string `json:"onStatefulSetUpdateFailureAction" yaml:"onStatefulSetUpdateFailureAction"` + + // Default values for ClickHouse user configuration + // 1. user/profile - string + // 2. user/quota - string + // 3. user/networks/ip - multiple strings + // 4. user/password - string + CHConfigUserDefaultProfile string `json:"chConfigUserDefaultProfile" yaml:"chConfigUserDefaultProfile"` + CHConfigUserDefaultQuota string `json:"chConfigUserDefaultQuota" yaml:"chConfigUserDefaultQuota"` + CHConfigUserDefaultNetworksIP []string `json:"chConfigUserDefaultNetworksIP" yaml:"chConfigUserDefaultNetworksIP"` + CHConfigUserDefaultPassword string `json:"chConfigUserDefaultPassword" yaml:"chConfigUserDefaultPassword"` + + CHConfigNetworksHostRegexpTemplate string `json:"chConfigNetworksHostRegexpTemplate" yaml:"chConfigNetworksHostRegexpTemplate"` + + // Username and Password to be used by operator to connect to ClickHouse instances + // for + // 1. Metrics requests + // 2. Schema maintenance + // User credentials can be specified in additional ClickHouse config files located in `chUsersConfigsPath` folder + CHScheme string `json:"chScheme" yaml:"chScheme"` + CHUsername string `json:"chUsername" yaml:"chUsername"` + CHPassword string `json:"chPassword" yaml:"chPassword"` + // Location of k8s Secret with username and password to be used by operator to connect to ClickHouse instances + // Can be used instead of explicitly specified username and password + CHCredentialsSecretNamespace string `json:"chCredentialsSecretNamespace" yaml:"chCredentialsSecretNamespace"` + CHCredentialsSecretName string `json:"chCredentialsSecretName" yaml:"chCredentialsSecretName"` + + // Port where to connect to ClickHouse instances to + CHPort int `json:"chPort" yaml:"chPort"` + + // Logger section + LogToStderr string `json:"logtostderr" yaml:"logtostderr"` + AlsoLogToStderr string `json:"alsologtostderr" yaml:"alsologtostderr"` + V string `json:"v" yaml:"v"` + StderrThreshold string `json:"stderrthreshold" yaml:"stderrthreshold"` + VModule string `json:"vmodule" yaml:"vmodule"` + LogBacktraceAt string `json:"log_backtrace_at" yaml:"log_backtrace_at"` + // Max number of concurrent reconciles in progress + ReconcileThreadsNumber int `json:"reconcileThreadsNumber" yaml:"reconcileThreadsNumber"` + ReconcileWaitExclude bool `json:"reconcileWaitExclude" yaml:"reconcileWaitExclude"` + ReconcileWaitInclude bool `json:"reconcileWaitInclude" yaml:"reconcileWaitInclude"` + + // When transferring annotations from the chi/chit.metadata to CHI objects, use these filters. + IncludeIntoPropagationAnnotations []string `json:"includeIntoPropagationAnnotations" yaml:"includeIntoPropagationAnnotations"` + ExcludeFromPropagationAnnotations []string `json:"excludeFromPropagationAnnotations" yaml:"excludeFromPropagationAnnotations"` + + // When transferring labels from the chi/chit.metadata to child objects, use these filters. + IncludeIntoPropagationLabels []string `json:"includeIntoPropagationLabels" yaml:"includeIntoPropagationLabels"` + ExcludeFromPropagationLabels []string `json:"excludeFromPropagationLabels" yaml:"excludeFromPropagationLabels"` + + // Whether to append *Scope* labels to StatefulSet and Pod. + AppendScopeLabelsString types.StringBool `json:"appendScopeLabels" yaml:"appendScopeLabels"` + + // Grace period for Pod termination. + TerminationGracePeriod int `json:"terminationGracePeriod" yaml:"terminationGracePeriod"` + // Revision history limit + RevisionHistoryLimit int `json:"revisionHistoryLimit" yaml:"revisionHistoryLimit"` +} + +// MergeFrom merges +func (c *OperatorConfig) MergeFrom(from *OperatorConfig) error { + if from == nil { + return nil + } + + if err := mergo.Merge(c, from, mergo.WithAppendSlice, mergo.WithOverride); err != nil { + return fmt.Errorf("FAIL merge config Error: %q", err) + } + + return nil +} + +// readCHITemplates build OperatorConfig.CHITemplate from template files content +func (c *OperatorConfig) readCHITemplates() (errs []error) { + // Read CHI template files + c.Template.CHI.Runtime.TemplateFiles = util.ReadFilesIntoMap(c.Template.CHI.Path, c.isCHITemplateExt) + + // Produce map of CHI templates out of CHI template files + for filename := range c.Template.CHI.Runtime.TemplateFiles { + template := new(ClickHouseInstallation) + if err := yaml.Unmarshal([]byte(c.Template.CHI.Runtime.TemplateFiles[filename]), template); err != nil { + // Unable to unmarshal - skip incorrect template + errs = append(errs, fmt.Errorf("FAIL readCHITemplates() unable to unmarshal file %s Error: %q", filename, err)) + continue // skip to the next template + } + // Template read successfully, let's append it to the list + c.enlistCHITemplate(template) + } + + return +} + +// enlistCHITemplate inserts template into templates catalog +func (c *OperatorConfig) enlistCHITemplate(template *ClickHouseInstallation) { + c.unlistCHITemplate(template) + + c.Template.CHI.Runtime.mutex.Lock() + defer c.Template.CHI.Runtime.mutex.Unlock() + + if !template.FoundIn(c.Template.CHI.Runtime.Templates) { + c.Template.CHI.Runtime.Templates = append(c.Template.CHI.Runtime.Templates, template) + } +} + +// unlistCHITemplate removes template from templates catalog +func (c *OperatorConfig) unlistCHITemplate(template *ClickHouseInstallation) { + c.Template.CHI.Runtime.mutex.Lock() + defer c.Template.CHI.Runtime.mutex.Unlock() + + // Nullify found template entry + for _, _template := range c.Template.CHI.Runtime.Templates { + if template.MatchFullName(_template.Namespace, _template.Name) { + // Mark for deletion + _template.Name = "" + _template.Namespace = "" + } + } + + // Compact the slice - exclude empty-named templates + var named []*ClickHouseInstallation + for _, _template := range c.Template.CHI.Runtime.Templates { + if !_template.MatchFullName("", "") { + named = append(named, _template) + } + } + + c.Template.CHI.Runtime.Templates = named +} + +// FindTemplate finds specified template within possibly specified namespace +func (c *OperatorConfig) FindTemplate(templateRef *TemplateRef, fallbackNamespace string) *ClickHouseInstallation { + c.Template.CHI.Runtime.mutex.RLock() + defer c.Template.CHI.Runtime.mutex.RUnlock() + + // Try to find direct match + for _, template := range c.Template.CHI.Runtime.Templates { + if template.MatchFullName(templateRef.Namespace, templateRef.Name) { + // Exact match, found the result + return template + } + } + + // Exact match is not possible. + // Let's try to find by name only in "predefined" namespace + + if templateRef.Namespace != "" { + // With fully-specified template namespace+name pair exact match is applicable only + // This is strange situation, however + return nil + } + + // Look for templates with specified name in "predefined" namespace + + for _, template := range c.Template.CHI.Runtime.Templates { + if template.MatchFullName(fallbackNamespace, templateRef.Name) { + // Found template with searched name in "predefined" namespace + return template + } + } + + return nil +} + +// GetAutoTemplates gets all auto templates. +// Auto templates are sorted alphabetically by tuple: namespace, name +func (c *OperatorConfig) GetAutoTemplates() []*ClickHouseInstallation { + c.Template.CHI.Runtime.mutex.RLock() + defer c.Template.CHI.Runtime.mutex.RUnlock() + + // Extract auto-templates from all templates listed + var autoTemplates []*ClickHouseInstallation + for _, _template := range c.Template.CHI.Runtime.Templates { + if _template.IsAuto() { + autoTemplates = append(autoTemplates, _template) + } + } + + // Prepare sorted unique list of namespaces + var namespaces []string + for _, _template := range autoTemplates { + // Append template's namespace to the list of namespaces + if !util.StringSliceContains(namespaces, _template.Namespace) { + namespaces = append(namespaces, _template.Namespace) + } + } + sort.Strings(namespaces) + + // Prepare sorted list of templates + var sortedTemplates []*ClickHouseInstallation + // Walk over sorted unique namespaces + for _, namespace := range namespaces { + // Prepare sorted unique list of names within this namespace + var names []string + for _, _template := range autoTemplates { + if _template.MatchNamespace(namespace) && !util.StringSliceContains(names, _template.Name) { + names = append(names, _template.Name) + } + } + sort.Strings(names) + + // Walk over sorted unique list of names within this namespace + // and append first unseen before template to the result list of templates + for _, name := range names { + for _, _template := range autoTemplates { + if _template.MatchFullName(namespace, name) && !_template.FoundIn(sortedTemplates) { + sortedTemplates = append(sortedTemplates, _template) + } + } + } + } + + return sortedTemplates +} + +// AddCHITemplate adds CHI template +func (c *OperatorConfig) AddCHITemplate(template *ClickHouseInstallation) { + c.enlistCHITemplate(template) +} + +// UpdateCHITemplate updates CHI template +func (c *OperatorConfig) UpdateCHITemplate(template *ClickHouseInstallation) { + c.enlistCHITemplate(template) +} + +// DeleteCHITemplate deletes CHI template +func (c *OperatorConfig) DeleteCHITemplate(template *ClickHouseInstallation) { + c.unlistCHITemplate(template) +} + +// Postprocess runs all postprocessors +func (c *OperatorConfig) Postprocess() { + c.normalize() + c.readClickHouseCustomConfigFiles() + c.readKeeperCustomConfigFiles() + c.readCHITemplates() + c.applyEnvVarParams() + c.applyDefaultWatchNamespace() +} + +func (c *OperatorConfig) normalizeSectionClickHouseConfigurationFile() { + // Process ClickHouse configuration files section + // Apply default paths in case nothing specified + util.PreparePath(&c.ClickHouse.Config.File.Path.Common, c.Runtime.ConfigFolderPath, CommonConfigDirClickHouse) + util.PreparePath(&c.ClickHouse.Config.File.Path.Host, c.Runtime.ConfigFolderPath, HostConfigDirClickHouse) + util.PreparePath(&c.ClickHouse.Config.File.Path.User, c.Runtime.ConfigFolderPath, UsersConfigDirClickHouse) +} + +func (c *OperatorConfig) normalizeSectionKeeperConfigurationFile() { + // Process Keeper configuration files section + // Apply default paths in case nothing specified + util.PreparePath(&c.Keeper.Config.File.Path.Common, c.Runtime.ConfigFolderPath, CommonConfigDirKeeper) + util.PreparePath(&c.Keeper.Config.File.Path.Host, c.Runtime.ConfigFolderPath, HostConfigDirKeeper) + util.PreparePath(&c.Keeper.Config.File.Path.User, c.Runtime.ConfigFolderPath, UsersConfigDirKeeper) +} + +func (c *OperatorConfig) normalizeSectionTemplate() { + p := c.Template.CHI.Policy + switch { + case p.Equals(OperatorConfigCHIPolicyReadOnStart): + c.Template.CHI.Policy = OperatorConfigCHIPolicyReadOnStart + case p.Equals(OperatorConfigCHIPolicyApplyOnNextReconcile): + c.Template.CHI.Policy = OperatorConfigCHIPolicyApplyOnNextReconcile + default: + c.Template.CHI.Policy = defaultOperatorConfigCHIPolicy + } + + // Process ClickHouseInstallation templates section + util.PreparePath(&c.Template.CHI.Path, c.Runtime.ConfigFolderPath, TemplatesDirClickHouse) +} + +func (c *OperatorConfig) normalizeSectionReconcileStatefulSet() { + // Process Create/Update section + + // Timeouts + if c.Reconcile.StatefulSet.Update.Timeout == 0 { + // Default update timeout in seconds + c.Reconcile.StatefulSet.Update.Timeout = defaultStatefulSetUpdateTimeout + } + + if c.Reconcile.StatefulSet.Update.PollInterval == 0 { + // Default polling period in seconds + c.Reconcile.StatefulSet.Update.PollInterval = defaultStatefulSetUpdatePollInterval + } + + // Default action on Create/Update failure - to keep system in previous state + + // Default Create Failure action - delete + if c.Reconcile.StatefulSet.Create.OnFailure == "" { + c.Reconcile.StatefulSet.Create.OnFailure = OnStatefulSetCreateFailureActionDelete + } + + // Default Updated Failure action - revert + if c.Reconcile.StatefulSet.Update.OnFailure == "" { + c.Reconcile.StatefulSet.Update.OnFailure = OnStatefulSetUpdateFailureActionRollback + } + + // Default Recreate actions - recreate + if c.Reconcile.StatefulSet.Recreate.OnDataLoss == "" { + c.Reconcile.StatefulSet.Recreate.OnDataLoss = OnStatefulSetRecreateOnDataLossActionRecreate + } + if c.Reconcile.StatefulSet.Recreate.OnUpdateFailure == "" { + c.Reconcile.StatefulSet.Recreate.OnUpdateFailure = OnStatefulSetRecreateOnUpdateFailureActionRecreate + } +} + +func (c *OperatorConfig) normalizeSectionReconcileHost() { + c.Reconcile.Host = c.Reconcile.Host.Normalize() +} + +func (c *OperatorConfig) normalizeSectionClickHouseConfigurationUserDefault() { + // Default values for ClickHouse user configuration + // 1. user/profile + // 2. user/quota + // 3. user/networks/ip + // 4. user/password + if c.ClickHouse.Config.User.Default.Profile == "" { + c.ClickHouse.Config.User.Default.Profile = defaultChConfigUserDefaultProfile + } + if c.ClickHouse.Config.User.Default.Quota == "" { + c.ClickHouse.Config.User.Default.Quota = defaultChConfigUserDefaultQuota + } + if len(c.ClickHouse.Config.User.Default.NetworksIP) == 0 { + c.ClickHouse.Config.User.Default.NetworksIP = []string{defaultChConfigUserDefaultNetworkIP} + } + if c.ClickHouse.Config.User.Default.Password == "" { + c.ClickHouse.Config.User.Default.Password = defaultChConfigUserDefaultPassword + } + + // chConfigNetworksHostRegexpTemplate +} + +func (c *OperatorConfig) normalizeSectionClickHouseAccess() { + // Username and Password to be used by operator to connect to ClickHouse instances for + // 1. Metrics requests + // 2. Schema maintenance + // User credentials can be specified in additional ClickHouse config files located in `chUsersConfigsPath` folder + switch strings.ToLower(c.ClickHouse.Access.Scheme) { + case ChSchemeHTTP: + c.ClickHouse.Access.Scheme = ChSchemeHTTP + case ChSchemeHTTPS: + c.ClickHouse.Access.Scheme = ChSchemeHTTPS + case ChSchemeAuto: + c.ClickHouse.Access.Scheme = ChSchemeAuto + default: + c.ClickHouse.Access.Scheme = defaultChScheme + } + if c.ClickHouse.Access.Username == "" { + c.ClickHouse.Access.Username = defaultChUsername + } + if c.ClickHouse.Access.Password == "" { + c.ClickHouse.Access.Password = defaultChPassword + } + if c.ClickHouse.Access.RootCA == "" { + c.ClickHouse.Access.RootCA = defaultChRootCA + } + // config.CHCredentialsSecretNamespace + // config.CHCredentialsSecretName + + // Overwrite credentials with data from the secret (if both username and password provided) + if (c.ClickHouse.Access.Secret.Runtime.Username != "") && (c.ClickHouse.Access.Secret.Runtime.Password != "") { + c.ClickHouse.Access.Username = c.ClickHouse.Access.Secret.Runtime.Username + c.ClickHouse.Access.Password = c.ClickHouse.Access.Secret.Runtime.Password + } + + if c.ClickHouse.Access.Port == 0 { + c.ClickHouse.Access.Port = defaultChPort + } + + // Timeouts + + if c.ClickHouse.Access.Timeouts.Connect == 0 { + c.ClickHouse.Access.Timeouts.Connect = defaultTimeoutConnect + } + // Adjust seconds to time.Duration + c.ClickHouse.Access.Timeouts.Connect = c.ClickHouse.Access.Timeouts.Connect * time.Second + + if c.ClickHouse.Access.Timeouts.Query == 0 { + c.ClickHouse.Access.Timeouts.Query = defaultTimeoutQuery + } + // Adjust seconds to time.Duration + c.ClickHouse.Access.Timeouts.Query = c.ClickHouse.Access.Timeouts.Query * time.Second + +} + +func (c *OperatorConfig) normalizeSectionClickHouseMetrics() { + if c.ClickHouse.Metrics.Timeouts.Collect == 0 { + c.ClickHouse.Metrics.Timeouts.Collect = defaultTimeoutCollect + } + // Adjust seconds to time.Duration + c.ClickHouse.Metrics.Timeouts.Collect = c.ClickHouse.Metrics.Timeouts.Collect * time.Second + + if c.ClickHouse.Metrics.TablesRegexp == "" { + c.ClickHouse.Metrics.TablesRegexp = defaultMetricsTablesRegexp + } +} + +func (c *OperatorConfig) normalizeSectionLogger() { + // Logtostderr string `json:"logtostderr" yaml:"logtostderr"` + // Alsologtostderr string `json:"alsologtostderr" yaml:"alsologtostderr"` + // V string `json:"v" yaml:"v"` + // Stderrthreshold string `json:"stderrthreshold" yaml:"stderrthreshold"` + // Vmodule string `json:"vmodule" yaml:"vmodule"` + // Log_backtrace_at string `json:"log_backtrace_at" yaml:"log_backtrace_at"` +} + +func (c *OperatorConfig) normalizeSectionReconcileRuntime() { + if c.Reconcile.Runtime.ThreadsNumber == 0 { + c.Reconcile.Runtime.ThreadsNumber = defaultReconcileCHIsThreadsNumber + } + if c.Reconcile.Runtime.ReconcileCHIsThreadsNumber == 0 { + c.Reconcile.Runtime.ReconcileCHIsThreadsNumber = defaultReconcileCHIsThreadsNumber + } + if c.Reconcile.Runtime.ReconcileShardsThreadsNumber == 0 { + c.Reconcile.Runtime.ReconcileShardsThreadsNumber = defaultReconcileShardsThreadsNumber + } + if c.Reconcile.Runtime.ReconcileShardsMaxConcurrencyPercent == 0 { + c.Reconcile.Runtime.ReconcileShardsMaxConcurrencyPercent = defaultReconcileShardsMaxConcurrencyPercent + } + + //reconcileWaitExclude: true + //reconcileWaitInclude: false +} + +func (c *OperatorConfig) normalizeSectionLabel() { + //config.IncludeIntoPropagationAnnotations + //config.ExcludeFromPropagationAnnotations + //config.IncludeIntoPropagationLabels + //config.ExcludeFromPropagationLabels + // Whether to append *Scope* labels to StatefulSet and Pod. + c.Label.Runtime.AppendScope = c.Label.AppendScopeString.Value() +} + +func (c *OperatorConfig) normalizeSectionStatefulSet() { + if c.StatefulSet.RevisionHistoryLimit == 0 { + c.StatefulSet.RevisionHistoryLimit = defaultRevisionHistoryLimit + } +} + +func (c *OperatorConfig) normalizeSectionPod() { + if c.Pod.TerminationGracePeriod == 0 { + c.Pod.TerminationGracePeriod = defaultTerminationGracePeriod + } +} + +// normalize() makes fully-and-correctly filled OperatorConfig +func (c *OperatorConfig) normalize() { + c.move() + c.Runtime.Namespace = os.Getenv(deployment.OPERATOR_POD_NAMESPACE) + + c.normalizeSectionClickHouseConfigurationFile() + c.normalizeSectionClickHouseConfigurationUserDefault() + c.normalizeSectionClickHouseAccess() + c.normalizeSectionClickHouseMetrics() + c.normalizeSectionKeeperConfigurationFile() + c.normalizeSectionTemplate() + c.normalizeSectionReconcileRuntime() + c.normalizeSectionReconcileStatefulSet() + c.normalizeSectionReconcileHost() + c.normalizeSectionLogger() + c.normalizeSectionLabel() + c.normalizeSectionStatefulSet() + c.normalizeSectionPod() +} + +// applyEnvVarParams applies ENV VARS over config +func (c *OperatorConfig) applyEnvVarParams() { + if ns := os.Getenv(deployment.WATCH_NAMESPACE); len(ns) > 0 { + // We have WATCH_NAMESPACE explicitly specified + c.Watch.Namespaces.Include = types.NewStrings([]string{ns}) + } + + if nss := os.Getenv(deployment.WATCH_NAMESPACES); len(nss) > 0 { + // We have WATCH_NAMESPACES explicitly specified + if namespaces := c.splitNamespaces(nss); len(namespaces) > 0 { + c.Watch.Namespaces.Include = types.NewStrings(namespaces) + } + } + + if nss := os.Getenv(deployment.WATCH_NAMESPACES_EXCLUDE); len(nss) > 0 { + // We have WATCH_NAMESPACES_EXCLUDE explicitly specified + if namespaces := c.splitNamespaces(nss); len(namespaces) > 0 { + c.Watch.Namespaces.Exclude = types.NewStrings(namespaces) + } + } +} + +func (c *OperatorConfig) splitNamespaces(combined string) (namespaces []string) { + candidates := strings.FieldsFunc(combined, func(r rune) bool { + return r == ':' || r == ',' + }) + for _, str := range candidates { + candidate := strings.TrimSpace(str) + if len(candidate) > 0 { + namespaces = append(namespaces, candidate) + } + } + return namespaces +} + +// applyDefaultWatchNamespace applies default watch namespace in case none specified earlier +func (c *OperatorConfig) applyDefaultWatchNamespace() { + // In case we have watched namespaces specified explicitly, all is fine + // In case we do not have watched namespaces specified, we need to decide, what namespace(s) to watch. + // In this case, there are two options: + // 1. Operator runs in 'kube-system' namespace - assume this is global installation, need to watch ALL namespaces + // 2. Operator runs in other (non kube-system) namespace - assume this is local installation, watch this namespace only + // Main idea is to watch in own namespace only in case no other specified explicitly and non-global deploy (not in 'kube-system') + + if c.Watch.Namespaces.Include.HasValue() { + // We have namespace(s) explicitly specified already, all is good + return + } + + // No namespace(s) specified explicitly, need to infer + + if c.Runtime.Namespace == meta.NamespaceSystem { + // Operator is running in system namespace + // Do nothing, we already have len(config.WatchNamespaces) == 0 + } else { + // Operator is running inside a namespace. Watch in it + c.Watch.Namespaces.Include = types.NewStrings([]string{ + c.Runtime.Namespace, + }) + } +} + +// readClickHouseCustomConfigFiles reads all extra user-specified ClickHouse config files +func (c *OperatorConfig) readClickHouseCustomConfigFiles() { + c.ClickHouse.Config.File.Runtime.CommonConfigFiles = util.ReadFilesIntoMap(c.ClickHouse.Config.File.Path.Common, c.isCHConfigExt) + c.ClickHouse.Config.File.Runtime.HostConfigFiles = util.ReadFilesIntoMap(c.ClickHouse.Config.File.Path.Host, c.isCHConfigExt) + c.ClickHouse.Config.File.Runtime.UsersConfigFiles = util.ReadFilesIntoMap(c.ClickHouse.Config.File.Path.User, c.isCHConfigExt) +} + +// readKeeperCustomConfigFiles reads all extra user-specified Keeper config files +func (c *OperatorConfig) readKeeperCustomConfigFiles() { + c.Keeper.Config.File.Runtime.CommonConfigFiles = util.ReadFilesIntoMap(c.Keeper.Config.File.Path.Common, c.isCHConfigExt) + c.Keeper.Config.File.Runtime.HostConfigFiles = util.ReadFilesIntoMap(c.Keeper.Config.File.Path.Host, c.isCHConfigExt) + c.Keeper.Config.File.Runtime.UsersConfigFiles = util.ReadFilesIntoMap(c.Keeper.Config.File.Path.User, c.isCHConfigExt) +} + +// isCHConfigExt returns true in case specified file has proper extension for a ClickHouse config file +func (c *OperatorConfig) isCHConfigExt(file string) bool { + switch util.ExtToLower(file) { + case ".xml": + return true + } + return false +} + +// isCHITemplateExt returns true in case specified file has proper extension for a CHI template config file +func (c *OperatorConfig) isCHITemplateExt(file string) bool { + switch util.ExtToLower(file) { + case ".yaml": + return true + case ".json": + return true + } + return false +} + +// String returns string representation of an OperatorConfig +func (c *OperatorConfig) String(hideCredentials bool) string { + conf := c + if hideCredentials { + conf = c.copyWithHiddenCredentials() + } + if bytes, err := yaml.Marshal(conf); err == nil { + return string(bytes) + } + if bytes, err := json.MarshalIndent(conf, "", " "); err == nil { + return string(bytes) + } + + return "" +} + +func (c *OperatorConfig) copyWithHiddenCredentials() *OperatorConfig { + conf := c.DeepCopy() + if conf.ClickHouse.Config.User.Default.Password != "" { + conf.ClickHouse.Config.User.Default.Password = PasswordReplacer + } + //conf.ClickHouse.Access.Username = UsernameReplacer + if conf.ClickHouse.Access.Password != "" { + conf.ClickHouse.Access.Password = PasswordReplacer + } + //conf.ClickHouse.Access.Secret.Runtime.Username = UsernameReplacer + if conf.ClickHouse.Access.Secret.Runtime.Password != "" { + conf.ClickHouse.Access.Secret.Runtime.Password = PasswordReplacer + } + + // DEPRECATED + conf.CHConfigUserDefaultPassword = PasswordReplacer + conf.CHUsername = UsernameReplacer + conf.CHPassword = PasswordReplacer + + return conf +} + +// IsNamespaceWatched returns whether specified namespace is in a list of watched +// TODO unify with GetInformerNamespace +func (c *OperatorConfig) IsNamespaceWatched(namespace string) bool { + return c.isNamespaceIncluded(namespace) && !c.isNamespaceExcluded(namespace) +} + +func (c *OperatorConfig) isNamespaceIncluded(namespace string) bool { + switch { + // In case no included namespaces specified - consider all namespaces included + case !c.Watch.Namespaces.Include.HasValue(): + return true + + // In case matches any included namespaces regexp specified - consider it included + case c.Watch.Namespaces.Include.Match(namespace): + return true + + // No match to explicitly specified set of namespace regexp - not included + default: + return false + } +} + +func (c *OperatorConfig) isNamespaceExcluded(namespace string) bool { + switch { + // In case no excluded namespaces specified - consider not excluded + case !c.Watch.Namespaces.Exclude.HasValue(): + return false + + // In case matches any excluded namespaces regexp specified - consider it excluded + case c.Watch.Namespaces.Exclude.Match(namespace): + return true + + // No match to explicitly specified set of namespace regexp - not excluded + default: + return false + } +} + +// GetInformerNamespace is a TODO stub +// Namespace where informers would watch notifications from +// The thing is that InformerFactory can accept only one parameter as watched namespace, +// be it explicitly specified namespace or empty line for "all namespaces". +// That's what conflicts with CHOp's approach to 'specify list of namespaces to watch in', having +// slice of namespaces (CHOp's approach) incompatible with "one namespace name" approach +// TODO unify with IsNamespaceWatched +// TODO unify approaches to multiple namespaces support +func (c *OperatorConfig) GetInformerNamespace() string { + // Namespace where informers would watch notifications from + namespace := meta.NamespaceAll + + if c.Watch.Namespaces.Include.Len() == 1 { + // We have exactly one watch namespace specified + // This scenario is implemented in go-client + // In any other case, just keep metav1.NamespaceAll + + // This contradicts current implementation of multiple namespaces in config's watchNamespaces field, + // but k8s has possibility to specify one/all namespaces only, no 'multiple namespaces' option + var labelRegexp = regexp.MustCompile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + if labelRegexp.MatchString(c.Watch.Namespaces.Include.First()) { + namespace = c.Watch.Namespaces.Include.First() + } + } + + return namespace +} + +// GetCacheNamespaces returns namespace list suitable for controller-runtime cache.Options.Namespaces. +// When all configured namespaces are valid DNS labels they are returned directly, allowing the cache +// to scope its watches to those namespaces only. If any namespace is a regexp pattern (or none are +// configured) the function falls back to []string{NamespaceAll} so the cache watches everything, +// relying on the per-reconcile namespace guard in the controller. +func (c *OperatorConfig) GetCacheNamespaces() []string { + if !c.Watch.Namespaces.Include.HasValue() { + return []string{meta.NamespaceAll} + } + var labelRegexp = regexp.MustCompile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$") + namespaces := c.Watch.Namespaces.Include.Value() + result := make([]string, 0, len(namespaces)) + for _, ns := range namespaces { + if !labelRegexp.MatchString(ns) { + // Contains a regexp pattern — can't enumerate exact namespaces, fall back to all + return []string{meta.NamespaceAll} + } + result = append(result, ns) + } + return result +} + +// GetLogLevel gets logger level +func (c *OperatorConfig) GetLogLevel() (log.Level, error) { + if i, err := strconv.Atoi(c.Logger.V); err == nil { + return log.Level(i), nil + } + return 0, fmt.Errorf("incorrect V value") +} + +// GetTerminationGracePeriod gets pointer to terminationGracePeriod, as expected by +// statefulSet.Spec.Template.Spec.TerminationGracePeriodSeconds +func (c *OperatorConfig) GetTerminationGracePeriod() *int64 { + terminationGracePeriod := int64(c.Pod.TerminationGracePeriod) + return &terminationGracePeriod +} + +// GetRevisionHistoryLimit gets pointer to revisionHistoryLimit, as expected by +// statefulSet.Spec.Template.Spec.RevisionHistoryLimit +func (c *OperatorConfig) GetRevisionHistoryLimit() *int32 { + revisionHistoryLimit := int32(c.StatefulSet.RevisionHistoryLimit) + return &revisionHistoryLimit +} + +func (c *OperatorConfig) move() { + // WatchNamespaces where operator watches for events + if len(c.WatchNamespaces) > 0 { + c.Watch.Namespaces.Include = types.NewStrings(c.WatchNamespaces) + } + + if c.CHCommonConfigsPath != "" { + c.ClickHouse.Config.File.Path.Common = c.CHCommonConfigsPath + } + if c.CHHostConfigsPath != "" { + c.ClickHouse.Config.File.Path.Host = c.CHHostConfigsPath + } + if c.CHUsersConfigsPath != "" { + c.ClickHouse.Config.File.Path.User = c.CHUsersConfigsPath + } + + // Path where to look for ClickHouseInstallation templates .yaml files + if c.CHITemplatesPath != "" { + c.Template.CHI.Path = c.CHITemplatesPath + } + // Create/Update StatefulSet behavior - for how long to wait for StatefulSet to reach new Generation + if c.StatefulSetUpdateTimeout != 0 { + c.Reconcile.StatefulSet.Update.Timeout = c.StatefulSetUpdateTimeout + } + // Create/Update StatefulSet behavior - for how long to sleep while polling StatefulSet to reach new Generation + if c.StatefulSetUpdatePollPeriod != 0 { + c.Reconcile.StatefulSet.Update.PollInterval = c.StatefulSetUpdatePollPeriod + } + + // Rolling Create/Update behavior + // StatefulSet create behavior - what to do in case StatefulSet can't reach new Generation + if c.OnStatefulSetCreateFailureAction != "" { + c.Reconcile.StatefulSet.Create.OnFailure = c.OnStatefulSetCreateFailureAction + } + // StatefulSet update behavior - what to do in case StatefulSet can't reach new Generation + if c.OnStatefulSetUpdateFailureAction != "" { + c.Reconcile.StatefulSet.Update.OnFailure = c.OnStatefulSetUpdateFailureAction + } + + // Default values for ClickHouse user configuration + // 1. user/profile - string + // 2. user/quota - string + // 3. user/networks/ip - multiple strings + // 4. user/password - string + if c.CHConfigUserDefaultProfile != "" { + c.ClickHouse.Config.User.Default.Profile = c.CHConfigUserDefaultProfile + } + if c.CHConfigUserDefaultQuota != "" { + c.ClickHouse.Config.User.Default.Quota = c.CHConfigUserDefaultQuota + } + if len(c.CHConfigUserDefaultNetworksIP) > 0 { + c.ClickHouse.Config.User.Default.NetworksIP = c.CHConfigUserDefaultNetworksIP + } + if c.CHConfigUserDefaultPassword != "" { + c.ClickHouse.Config.User.Default.Password = c.CHConfigUserDefaultPassword + } + + if c.CHConfigNetworksHostRegexpTemplate != "" { + c.ClickHouse.Config.Network.HostRegexpTemplate = c.CHConfigNetworksHostRegexpTemplate + } + + // Username and Password to be used by operator to connect to ClickHouse instances + // for + // 1. Metrics requests + // 2. Schema maintenance + // User credentials can be specified in additional ClickHouse config files located in `chUsersConfigsPath` folder + if c.CHScheme != "" { + c.ClickHouse.Access.Password = c.CHScheme + } + if c.CHUsername != "" { + c.ClickHouse.Access.Username = c.CHUsername + } + if c.CHPassword != "" { + c.ClickHouse.Access.Password = c.CHPassword + } + // Location of k8s Secret with username and password to be used by operator to connect to ClickHouse instances + // Can be used instead of explicitly specified username and password + if c.CHCredentialsSecretNamespace != "" { + c.ClickHouse.Access.Secret.Namespace = c.CHCredentialsSecretNamespace + } + if c.CHCredentialsSecretName != "" { + c.ClickHouse.Access.Secret.Name = c.CHCredentialsSecretName + } + + // Port where to connect to ClickHouse instances to + if c.CHPort != 0 { + c.ClickHouse.Access.Port = c.CHPort + } + + // Logger section + if c.LogToStderr != "" { + c.Logger.LogToStderr = c.LogToStderr + } + if c.AlsoLogToStderr != "" { + c.Logger.AlsoLogToStderr = c.AlsoLogToStderr + } + if c.V != "" { + c.Logger.V = c.V + } + if c.StderrThreshold != "" { + c.Logger.StderrThreshold = c.StderrThreshold + } + if c.VModule != "" { + c.Logger.VModule = c.VModule + } + if c.LogBacktraceAt != "" { + c.Logger.LogBacktraceAt = c.LogBacktraceAt + } + // Max number of concurrent reconciles in progress + if c.ReconcileThreadsNumber != 0 { + c.Reconcile.Runtime.ThreadsNumber = c.ReconcileThreadsNumber + } + if c.ReconcileWaitExclude { + c.Reconcile.Host.Wait.Exclude = c.Reconcile.Host.Wait.Exclude.From(c.ReconcileWaitExclude) + } + if c.ReconcileWaitInclude { + c.Reconcile.Host.Wait.Include = c.Reconcile.Host.Wait.Include.From(c.ReconcileWaitInclude) + } + + // When transferring annotations from the chi/chit.metadata to CHI objects, use these filters. + if len(c.IncludeIntoPropagationAnnotations) > 0 { + c.Annotation.Include = c.IncludeIntoPropagationAnnotations + } + if len(c.ExcludeFromPropagationAnnotations) > 0 { + c.Annotation.Exclude = c.ExcludeFromPropagationAnnotations + } + + // When transferring labels from the chi/chit.metadata to child objects, use these filters. + if len(c.IncludeIntoPropagationLabels) > 0 { + c.Label.Include = c.IncludeIntoPropagationLabels + } + if len(c.ExcludeFromPropagationLabels) > 0 { + c.Label.Exclude = c.ExcludeFromPropagationLabels + } + + // Whether to append *Scope* labels to StatefulSet and Pod. + if c.AppendScopeLabelsString != "" { + c.Label.AppendScopeString = c.AppendScopeLabelsString + } + + // Grace period for Pod termination. + if c.TerminationGracePeriod != 0 { + c.Pod.TerminationGracePeriod = c.TerminationGracePeriod + } + // Revision history limit + if c.RevisionHistoryLimit != 0 { + c.StatefulSet.RevisionHistoryLimit = c.RevisionHistoryLimit + } +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_defaults.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_defaults.go new file mode 100644 index 00000000..21f7259c --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_defaults.go @@ -0,0 +1,66 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + +// Defaults defines defaults section of .spec +type Defaults struct { + ReplicasUseFQDN *types.StringBool `json:"replicasUseFQDN,omitempty" yaml:"replicasUseFQDN,omitempty"` + DistributedDDL *DistributedDDL `json:"distributedDDL,omitempty" yaml:"distributedDDL,omitempty"` + StorageManagement *StorageManagement `json:"storageManagement,omitempty" yaml:"storageManagement,omitempty"` + Templates *TemplatesList `json:"templates,omitempty" yaml:"templates,omitempty"` +} + +// NewDefaults creates new Defaults object +func NewDefaults() *Defaults { + return new(Defaults) +} + +func (d *Defaults) GetDistributedDDL() *DistributedDDL { + if d == nil { + return nil + } + return d.DistributedDDL +} + +// MergeFrom merges from specified object +func (defaults *Defaults) MergeFrom(from *Defaults, _type MergeType) *Defaults { + if from == nil { + return defaults + } + + if defaults == nil { + defaults = NewDefaults() + } + + switch _type { + case MergeTypeFillEmptyValues: + if !from.ReplicasUseFQDN.HasValue() { + defaults.ReplicasUseFQDN = defaults.ReplicasUseFQDN.MergeFrom(from.ReplicasUseFQDN) + } + case MergeTypeOverrideByNonEmptyValues: + if from.ReplicasUseFQDN.HasValue() { + // Override by non-empty values only + defaults.ReplicasUseFQDN = defaults.ReplicasUseFQDN.MergeFrom(from.ReplicasUseFQDN) + } + } + + defaults.DistributedDDL = defaults.DistributedDDL.MergeFrom(from.DistributedDDL, _type) + defaults.StorageManagement = defaults.StorageManagement.MergeFrom(from.StorageManagement, _type) + defaults.Templates = defaults.Templates.MergeFrom(from.Templates, _type) + + return defaults +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_distributed_ddl.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_distributed_ddl.go new file mode 100644 index 00000000..8e93a795 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_distributed_ddl.go @@ -0,0 +1,66 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// DistributedDDL defines distributedDDL section of .spec.defaults +type DistributedDDL struct { + Profile string `json:"profile,omitempty" yaml:"profile"` +} + +// NewDistributedDDL creates new DistributedDDL +func NewDistributedDDL() *DistributedDDL { + return new(DistributedDDL) +} + +// HasProfile checks whether profile is present +func (d *DistributedDDL) HasProfile() bool { + if d == nil { + return false + } + return len(d.Profile) > 0 +} + +// GetProfile gets profile +func (d *DistributedDDL) GetProfile() string { + if d == nil { + return "" + } + return d.Profile +} + +// MergeFrom merges from specified source +func (d *DistributedDDL) MergeFrom(from *DistributedDDL, _type MergeType) *DistributedDDL { + if from == nil { + return d + } + + if d == nil { + d = NewDistributedDDL() + } + + switch _type { + case MergeTypeFillEmptyValues: + if d.Profile == "" { + d.Profile = from.Profile + } + case MergeTypeOverrideByNonEmptyValues: + if from.Profile != "" { + // Override by non-empty values only + d.Profile = from.Profile + } + } + + return d +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_host.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_host.go new file mode 100644 index 00000000..8f9838e3 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_host.go @@ -0,0 +1,667 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + apps "k8s.io/api/apps/v1" + core "k8s.io/api/core/v1" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/swversion" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" +) + +// Host defines host (a data replica within a shard) of .spec.configuration.clusters[n].shards[m] +type Host struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + HostSecure `json:",inline" yaml:",inline"` + HostPorts `json:",inline" yaml:",inline"` + HostSettings `json:",inline" yaml:",inline"` + Templates *TemplatesList `json:"templates,omitempty" yaml:"templates,omitempty"` + + Runtime HostRuntime `json:"-" yaml:"-"` +} + +type HostSecure struct { + Insecure *types.StringBool `json:"insecure,omitempty" yaml:"insecure,omitempty"` + Secure *types.StringBool `json:"secure,omitempty" yaml:"secure,omitempty"` +} + +type HostPorts struct { + // DEPRECATED - to be removed soon + Port *types.Int32 `json:"port,omitempty" yaml:"port,omitempty"` + + TCPPort *types.Int32 `json:"tcpPort,omitempty" yaml:"tcpPort,omitempty"` + TLSPort *types.Int32 `json:"tlsPort,omitempty" yaml:"tlsPort,omitempty"` + HTTPPort *types.Int32 `json:"httpPort,omitempty" yaml:"httpPort,omitempty"` + HTTPSPort *types.Int32 `json:"httpsPort,omitempty" yaml:"httpsPort,omitempty"` + InterserverHTTPPort *types.Int32 `json:"interserverHTTPPort,omitempty" yaml:"interserverHTTPPort,omitempty"` + ZKPort *types.Int32 `json:"zkPort,omitempty" yaml:"zkPort,omitempty"` + RaftPort *types.Int32 `json:"raftPort,omitempty" yaml:"raftPort,omitempty"` +} + +type HostSettings struct { + Settings *Settings `json:"settings,omitempty" yaml:"settings,omitempty"` + Files *Settings `json:"files,omitempty" yaml:"files,omitempty"` +} + +type HostRuntime struct { + // Internal data + Address HostAddress `json:"-" yaml:"-"` + Version *swversion.SoftWareVersion `json:"-" yaml:"-"` + reconcileAttributes *types.ReconcileAttributes `json:"-" yaml:"-" testdiff:"ignore"` + replicas *types.Int32 `json:"-" yaml:"-"` + hasData bool `json:"-" yaml:"-"` + + // CurStatefulSet is a current stateful set, fetched from k8s + CurStatefulSet *apps.StatefulSet `json:"-" yaml:"-" testdiff:"ignore"` + // DesiredStatefulSet is a desired stateful set - reconcile target + DesiredStatefulSet *apps.StatefulSet `json:"-" yaml:"-" testdiff:"ignore"` + + cr ICustomResource `json:"-" yaml:"-" testdiff:"ignore"` +} + +func (r *HostRuntime) GetAddress() IHostAddress { + if r == nil { + return (*HostAddress)(nil) + } + return &r.Address +} + +func (r *HostRuntime) SetCR(cr ICustomResource) { + if r == nil { + return + } + r.cr = cr +} + +func (r *HostRuntime) GetCR() ICustomResource { + if r == nil { + return nil + } + return r.cr +} + +func (host *Host) GetRuntime() IHostRuntime { + if host == nil { + return (*HostRuntime)(nil) + } + return &host.Runtime +} + +func (host *Host) GetTemplates() *TemplatesList { + if host == nil { + return nil + } + return host.Templates +} + +func (host *Host) SetTemplates(tl *TemplatesList) { + if host == nil { + return + } + host.Templates = tl +} + +// GetReconcileAttributes is an ensurer getter +func (host *Host) GetReconcileAttributes() *types.ReconcileAttributes { + if host == nil { + return nil + } + if host.Runtime.reconcileAttributes == nil { + host.Runtime.reconcileAttributes = types.NewReconcileAttributes() + } + return host.Runtime.reconcileAttributes +} + +// InheritSettingsFrom inherits settings from specified shard and replica +func (host *Host) InheritSettingsFrom(sources ...any) { + if host == nil { + return + } + for _, source := range sources { + switch typed := source.(type) { + case IShard: + shard := typed + if shard.HasSettings() { + host.Settings = host.Settings.MergeFrom(shard.GetSettings()) + } + case IReplica: + replica := typed + if replica.HasSettings() { + host.Settings = host.Settings.MergeFrom(replica.GetSettings()) + } + } + } +} + +// InheritFilesFrom inherits files from specified shard and replica +func (host *Host) InheritFilesFrom(sources ...any) { + if host == nil { + return + } + for _, source := range sources { + switch typed := source.(type) { + case IShard: + shard := typed + if shard.HasFiles() { + host.Files = host.Files.MergeFrom(shard.GetFiles()) + } + case IReplica: + replica := typed + if replica.HasFiles() { + host.Files = host.Files.MergeFrom(replica.GetFiles()) + } + } + } +} + +// InheritTemplatesFrom inherits templates from specified shard, replica or template +func (host *Host) InheritTemplatesFrom(sources ...any) { + if host == nil { + return + } + for _, source := range sources { + switch typed := source.(type) { + case IShard: + shard := typed + if shard.HasTemplates() { + host.SetTemplates(host.GetTemplates().MergeFrom(shard.GetTemplates(), MergeTypeFillEmptyValues)) + } + case IReplica: + replica := typed + if replica.HasTemplates() { + host.SetTemplates(host.GetTemplates().MergeFrom(replica.GetTemplates(), MergeTypeFillEmptyValues)) + } + case *HostTemplate: + template := typed + if template != nil { + host.SetTemplates(host.GetTemplates().MergeFrom(template.Spec.GetTemplates(), MergeTypeFillEmptyValues)) + } + } + } + + host.GetTemplates().HandleDeprecatedFields() +} + +// MergeFrom merges from specified host +func (host *Host) MergeFrom(from *Host) { + if (host == nil) || (from == nil) { + return + } + + host.Insecure = host.Insecure.MergeFrom(from.Insecure) + host.Secure = host.Secure.MergeFrom(from.Secure) + + if !host.TCPPort.HasValue() { + host.TCPPort.MergeFrom(from.TCPPort) + } + if !host.TLSPort.HasValue() { + host.TLSPort.MergeFrom(from.TLSPort) + } + if !host.HTTPPort.HasValue() { + host.HTTPPort.MergeFrom(from.HTTPPort) + } + if !host.HTTPSPort.HasValue() { + host.HTTPSPort.MergeFrom(from.HTTPSPort) + } + if !host.InterserverHTTPPort.HasValue() { + host.InterserverHTTPPort.MergeFrom(from.InterserverHTTPPort) + } + if !host.ZKPort.HasValue() { + host.ZKPort.MergeFrom(from.ZKPort) + } + if !host.RaftPort.HasValue() { + host.RaftPort.MergeFrom(from.RaftPort) + } + + host.SetTemplates(host.GetTemplates().MergeFrom(from.GetTemplates(), MergeTypeFillEmptyValues)) + host.GetTemplates().HandleDeprecatedFields() +} + +// GetHostTemplate gets host template +func (host *Host) GetHostTemplate() (*HostTemplate, bool) { + if !host.GetTemplates().HasHostTemplate() { + return nil, false + } + name := host.GetTemplates().GetHostTemplate() + return host.GetCR().GetHostTemplate(name) +} + +// GetPodTemplate gets pod template +func (host *Host) GetPodTemplate() (*PodTemplate, bool) { + if !host.GetTemplates().HasPodTemplate() { + return nil, false + } + name := host.GetTemplates().GetPodTemplate() + return host.GetCR().GetPodTemplate(name) +} + +// GetServiceTemplate gets service template +func (host *Host) GetServiceTemplate() (*ServiceTemplate, bool) { + if !host.GetTemplates().HasReplicaServiceTemplate() { + return nil, false + } + name := host.GetTemplates().GetReplicaServiceTemplate() + return host.GetCR().GetServiceTemplate(name) +} + +// GetStatefulSetReplicasNum gets stateful set replica num +func (host *Host) GetStatefulSetReplicasNum(shutdown bool) *int32 { + var num int32 = 0 + switch { + case shutdown: + num = 0 + case host.IsStopped(): + num = 0 + case host.Runtime.replicas.HasValue(): + num = host.Runtime.replicas.Value() + default: + num = 1 + } + return &num +} + +// GetSettings gets settings +func (host *Host) GetSettings() *Settings { + if host == nil { + return nil + } + return host.Settings +} + +// GetZookeeper gets zookeeper +func (host *Host) GetZookeeper() *ZookeeperConfig { + cluster := host.GetCluster() + return cluster.GetZookeeper() +} + +// GetName gets name +func (host *Host) GetName() string { + if host == nil { + return "host-is-nil" + } + return host.Name +} + +// HasName checks whether host has a name +func (host *Host) HasName() bool { + if host == nil { + return false + } + return len(host.GetName()) > 0 +} + +// SetName is a setter +func (host *Host) SetName(name string) { + if host == nil { + return + } + host.Name = name +} + +// GetCR gets CHI +func (host *Host) GetCR() ICustomResource { + return host.GetRuntime().GetCR() +} + +// HasCR checks whether host has CHI +func (host *Host) HasCR() bool { + return host.GetCR() != nil +} + +func (host *Host) SetCR(chi ICustomResource) { + host.GetRuntime().SetCR(chi) +} + +// GetCluster gets cluster +func (host *Host) GetCluster() ICluster { + // Host has to have filled Address + return host.GetCR().FindCluster(host.Runtime.Address.ClusterName) +} + +// GetShard gets shard +func (host *Host) GetShard() IShard { + // Host has to have filled Address + return host.GetCR().FindShard(host.Runtime.Address.ClusterName, host.Runtime.Address.ShardName) +} + +// GetAncestor gets ancestor of a host +func (host *Host) GetAncestor() *Host { + if !host.HasAncestorCR() { + return nil + } + return host.GetAncestorCR().FindHost( + host.Runtime.Address.ClusterName, + host.Runtime.Address.ShardName, + host.Runtime.Address.HostName, + ) +} + +// HasAncestor checks whether host has an ancestor +func (host *Host) HasAncestor() bool { + return host.GetAncestor() != nil +} + +// GetAncestorCR gets ancestor of a host +func (host *Host) GetAncestorCR() ICustomResource { + return host.GetCR().GetAncestor() +} + +// HasAncestorCR checks whether host has an ancestor +func (host *Host) HasAncestorCR() bool { + return host.GetAncestorCR().IsNonZero() +} + +// WalkVolumeClaimTemplates walks VolumeClaimTemplate(s) +func (host *Host) WalkVolumeClaimTemplates(f func(template *VolumeClaimTemplate)) { + host.GetCR().WalkVolumeClaimTemplates(f) +} + +// IsStopped checks whether host is stopped +func (host *Host) IsStopped() bool { + return host.GetCR().IsStopped() +} + +// IsTroubleshoot checks whether host is in troubleshoot +func (host *Host) IsTroubleshoot() bool { + return host.GetCR().IsTroubleshoot() +} + +// IsInNewCluster checks whether host is in a new cluster +// TODO unify with model HostIsNewOne +func (host *Host) IsInNewCluster() bool { + return !host.HasAncestor() && (host.GetCR().IEnsureStatus().GetHostsCount() == host.GetCR().IEnsureStatus().GetHostsAddedCount()) +} + +// WhichStatefulSet specifies which StatefulSet we are going to process in host functions +type WhichStatefulSet string + +const ( + // CurStatefulSet specifies current StatefulSet to be processed + CurStatefulSet WhichStatefulSet = "cur" + // DesiredStatefulSet specifies desired StatefulSet to be processed + DesiredStatefulSet WhichStatefulSet = "desired" +) + +// CurStatefulSet checks whether WhichStatefulSet is a current one +func (w WhichStatefulSet) CurStatefulSet() bool { + return w == CurStatefulSet +} + +// DesiredStatefulSet checks whether WhichStatefulSet is a desired one +func (w WhichStatefulSet) DesiredStatefulSet() bool { + return w == DesiredStatefulSet +} + +// WalkVolumeMounts walks VolumeMount(s) +func (host *Host) WalkVolumeMounts(which WhichStatefulSet, f func(volumeMount *core.VolumeMount)) { + if host == nil { + return + } + + var sts *apps.StatefulSet + switch { + case which.DesiredStatefulSet(): + if !host.HasDesiredStatefulSet() { + return + } + sts = host.Runtime.DesiredStatefulSet + case which.CurStatefulSet(): + if !host.HasCurStatefulSet() { + return + } + sts = host.Runtime.CurStatefulSet + default: + return + } + + // TODO ensure sts.Spec.Template.Spec.Containers + + for i := range sts.Spec.Template.Spec.Containers { + container := &sts.Spec.Template.Spec.Containers[i] + for j := range container.VolumeMounts { + volumeMount := &container.VolumeMounts[j] + f(volumeMount) + } + } +} + +// GetVolumeMount gets VolumeMount by the name +//func (host *Host) GetVolumeMount(volumeMountName string) (vm *corev1.VolumeMount, ok bool) { +// host.WalkVolumeMounts(func(volumeMount *corev1.VolumeMount) { +// if volumeMount.Name == volumeMountName { +// vm = volumeMount +// ok = true +// } +// }) +// return +//} + +// IsSecure checks whether the host requires secure communication +func (host *Host) IsSecure() bool { + if host == nil { + return false + } + + // Personal host settings take priority + if host.Secure.HasValue() { + return host.Secure.Value() + } + + // No personal value - fallback to cluster value + if host.GetCluster().GetSecure().HasValue() { + return host.GetCluster().GetSecure().Value() + } + + // No cluster value - host should not expose secure + return false +} + +// IsInsecure checks whether the host requires insecure communication +func (host *Host) IsInsecure() bool { + if host == nil { + return false + } + + // Personal host settings take priority + if host.Insecure.HasValue() { + return host.Insecure.Value() + } + + // No personal value - fallback to cluster value + if host.GetCluster().GetInsecure().HasValue() { + return host.GetCluster().GetInsecure().Value() + } + + // No cluster value - host should expose insecure + return true +} + +// IsFirstInCR checks whether the host is the first host of the whole CHI +func (host *Host) IsFirstInCR() bool { + if host == nil { + return false + } + + return host.Runtime.Address.CHIScopeIndex == 0 +} + +// IsFirstInCluster checks whether the host is the first host of the cluster +func (host *Host) IsFirstInCluster() bool { + if host == nil { + return false + } + + return host.Runtime.Address.ClusterScopeIndex == 0 +} + +// IsFirst checks whether the host is the last host of the whole CHI +func (host *Host) IsLast() bool { + if host == nil { + return false + } + + return host.Runtime.Address.CHIScopeIndex == (host.GetCR().HostsCount() - 1) +} + +// HasCurStatefulSet checks whether host has CurStatefulSet +func (host *Host) HasCurStatefulSet() bool { + if host == nil { + return false + } + + return host.Runtime.CurStatefulSet != nil +} + +// HasDesiredStatefulSet checks whether host has DesiredStatefulSet +func (host *Host) HasDesiredStatefulSet() bool { + if host == nil { + return false + } + + return host.Runtime.DesiredStatefulSet != nil +} + +const ( + ChDefaultPortName = "port" + ChDefaultPortNumber = int32(9000) + + // ClickHouse open ports names and values + ChDefaultTCPPortName = "tcp" + ChDefaultTCPPortNumber = int32(9000) + ChDefaultTLSPortName = "secureclient" + ChDefaultTLSPortNumber = int32(9440) + ChDefaultHTTPPortName = "http" + ChDefaultHTTPPortNumber = int32(8123) + ChDefaultHTTPSPortName = "https" + ChDefaultHTTPSPortNumber = int32(8443) + ChDefaultInterserverHTTPPortName = "interserver" + ChDefaultInterserverHTTPPortNumber = int32(9009) + + // Keeper open ports names and values + KpDefaultZKPortName = "zk" + KpDefaultZKPortNumber = int32(2181) + KpDefaultRaftPortName = "raft" + KpDefaultRaftPortNumber = int32(9444) +) + +func (host *Host) WalkPorts(f func(name string, port *types.Int32, protocol core.Protocol) bool) { + if host == nil { + return + } + if f(ChDefaultPortName, host.Port, core.ProtocolTCP) { + return + } + if f(ChDefaultTCPPortName, host.TCPPort, core.ProtocolTCP) { + return + } + if f(ChDefaultTLSPortName, host.TLSPort, core.ProtocolTCP) { + return + } + if f(ChDefaultHTTPPortName, host.HTTPPort, core.ProtocolTCP) { + return + } + if f(ChDefaultHTTPSPortName, host.HTTPSPort, core.ProtocolTCP) { + return + } + if f(ChDefaultInterserverHTTPPortName, host.InterserverHTTPPort, core.ProtocolTCP) { + return + } + if f(KpDefaultZKPortName, host.ZKPort, core.ProtocolTCP) { + return + } + if f(KpDefaultRaftPortName, host.RaftPort, core.ProtocolTCP) { + return + } +} + +func (host *Host) WalkSpecifiedPorts(f func(name string, port *types.Int32, protocol core.Protocol) bool) { + host.WalkPorts( + func(_name string, _port *types.Int32, _protocol core.Protocol) bool { + if _port.HasValue() { + // Port is explicitly specified - call provided function on it + return f(_name, _port, _protocol) + } + // Do not break, continue iterating + return false + }, + ) +} + +func (host *Host) AppendSpecifiedPortsToContainer(container *core.Container) { + // Walk over all assigned ports of the host and append each port to the list of container's ports + host.WalkSpecifiedPorts( + func(name string, port *types.Int32, protocol core.Protocol) bool { + // Append assigned port to the list of container's ports + container.Ports = append(container.Ports, + core.ContainerPort{ + Name: name, + ContainerPort: port.Value(), + Protocol: protocol, + }, + ) + // Do not abort, continue iterating + return false + }, + ) +} + +func (host *Host) HasListedReplicaCaughtUp(name string) bool { + return util.InArray( + util.NormalizeFQDN(name), + host.GetCR().IEnsureStatus().GetHostsWithReplicaCaughtUp(), + ) +} + +func (host *Host) HasListedTablesCreated(name string) bool { + return util.InArray( + util.NormalizeFQDN(name), + host.GetCR().IEnsureStatus().GetHostsWithTablesCreated(), + ) +} + +func (host *Host) HasData() bool { + if host == nil { + return false + } + return host.Runtime.hasData +} + +func (host *Host) SetHasData(hasData bool) { + if host == nil { + return + } + host.Runtime.hasData = hasData +} + +func (host *Host) IsZero() bool { + return host == nil +} + +func (host *Host) IsNonZero() bool { + return host != nil +} + +func (host *Host) ShouldIncludeIntoCluster() bool { + switch { + case host.IsStopped(): + return false + case host.GetCluster().HostsCount() < 2: + return false + default: + return true + } +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_host_address.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_host_address.go new file mode 100644 index 00000000..84f32d82 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_host_address.go @@ -0,0 +1,223 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import "fmt" + +// HostAddress defines address of a host within ClickHouseInstallation +type HostAddress struct { + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + StatefulSet string `json:"statefulSet,omitempty" yaml:"statefulSet,omitempty"` + FQDN string `json:"fqdn,omitempty" yaml:"fqdn,omitempty"` + CHIName string `json:"chiName,omitempty" yaml:"chiName,omitempty"` + ClusterName string `json:"clusterName,omitempty" yaml:"clusterName,omitempty"` + ClusterIndex int `json:"clusterIndex,omitempty" yaml:"clusterIndex,omitempty"` + ShardName string `json:"shardName,omitempty" yaml:"shardName,omitempty"` + ShardIndex int `json:"shardIndex,omitempty" yaml:"shardIndex,omitempty"` + ShardScopeIndex int `json:"shardScopeIndex,omitempty" yaml:"shardScopeIndex,omitempty"` + ReplicaName string `json:"replicaName,omitempty" yaml:"replicaName,omitempty"` + ReplicaIndex int `json:"replicaIndex,omitempty" yaml:"replicaIndex,omitempty"` + ReplicaScopeIndex int `json:"replicaScopeIndex,omitempty" yaml:"replicaScopeIndex,omitempty"` + HostName string `json:"hostName,omitempty" yaml:"hostName,omitempty"` + CHIScopeIndex int `json:"chiScopeIndex,omitempty" yaml:"chiScopeIndex,omitempty"` + CHIScopeCycleSize int `json:"chiScopeCycleSize,omitempty" yaml:"chiScopeCycleSize,omitempty"` + CHIScopeCycleIndex int `json:"chiScopeCycleIndex,omitempty" yaml:"chiScopeCycleIndex,omitempty"` + CHIScopeCycleOffset int `json:"chiScopeCycleOffset,omitempty" yaml:"chiScopeCycleOffset,omitempty"` + ClusterScopeIndex int `json:"clusterScopeIndex,omitempty" yaml:"clusterScopeIndex,omitempty"` + ClusterScopeCycleSize int `json:"clusterScopeCycleSize,omitempty" yaml:"clusterScopeCycleSize,omitempty"` + ClusterScopeCycleIndex int `json:"clusterScopeCycleIndex,omitempty" yaml:"clusterScopeCycleIndex,omitempty"` + ClusterScopeCycleOffset int `json:"clusterScopeCycleOffset,omitempty" yaml:"clusterScopeCycleOffset,omitempty"` +} + +func (a *HostAddress) GetNamespace() string { + return a.Namespace +} + +func (a *HostAddress) SetNamespace(namespace string) { + a.Namespace = namespace +} + +func (a *HostAddress) GetStatefulSet() string { + return a.StatefulSet +} + +func (a *HostAddress) GetFQDN() string { + return a.FQDN +} + +func (a *HostAddress) GetCRName() string { + return a.CHIName +} + +func (a *HostAddress) SetCRName(name string) { + a.CHIName = name +} + +func (a *HostAddress) GetClusterName() string { + return a.ClusterName +} + +func (a *HostAddress) SetClusterName(name string) { + a.ClusterName = name +} + +func (a *HostAddress) GetClusterIndex() int { + return a.ClusterIndex +} + +func (a *HostAddress) SetClusterIndex(index int) { + a.ClusterIndex = index +} + +func (a *HostAddress) GetShardName() string { + return a.ShardName +} + +func (a *HostAddress) SetShardName(name string) { + a.ShardName = name +} + +func (a *HostAddress) GetShardIndex() int { + return a.ShardIndex +} + +func (a *HostAddress) SetShardIndex(index int) { + a.ShardIndex = index +} + +func (a *HostAddress) GetShardScopeIndex() int { + return a.ShardScopeIndex +} + +func (a *HostAddress) SetShardScopeIndex(index int) { + a.ShardScopeIndex = index +} + +func (a *HostAddress) GetReplicaName() string { + return a.ReplicaName +} + +func (a *HostAddress) SetReplicaName(name string) { + a.ReplicaName = name +} + +func (a *HostAddress) GetReplicaIndex() int { + return a.ReplicaIndex +} + +func (a *HostAddress) SetReplicaIndex(index int) { + a.ReplicaIndex = index +} + +func (a *HostAddress) GetReplicaScopeIndex() int { + return a.ReplicaScopeIndex +} + +func (a *HostAddress) SetReplicaScopeIndex(index int) { + a.ReplicaScopeIndex = index +} + +func (a *HostAddress) GetHostName() string { + return a.HostName +} + +func (a *HostAddress) SetHostName(name string) { + a.HostName = name +} + +func (a *HostAddress) GetCRScopeIndex() int { + return a.CHIScopeIndex +} + +func (a *HostAddress) SetCRScopeIndex(index int) { + a.CHIScopeIndex = index +} + +func (a *HostAddress) GetCRScopeCycleSize() int { + return a.CHIScopeCycleSize +} + +func (a *HostAddress) SetCRScopeCycleSize(size int) { + a.CHIScopeCycleSize = size +} + +func (a *HostAddress) GetCRScopeCycleIndex() int { + return a.CHIScopeCycleIndex +} + +func (a *HostAddress) SetCRScopeCycleIndex(index int) { + a.CHIScopeCycleIndex = index +} + +func (a *HostAddress) GetCRScopeCycleOffset() int { + return a.CHIScopeCycleOffset +} + +func (a *HostAddress) SetCRScopeCycleOffset(offset int) { + a.CHIScopeCycleOffset = offset +} + +func (a *HostAddress) GetClusterScopeIndex() int { + return a.ClusterScopeIndex +} + +func (a *HostAddress) SetClusterScopeIndex(index int) { + a.ClusterScopeIndex = index +} + +func (a *HostAddress) GetClusterScopeCycleSize() int { + return a.ClusterScopeCycleSize +} + +func (a *HostAddress) SetClusterScopeCycleSize(size int) { + a.ClusterScopeCycleSize = size +} + +func (a *HostAddress) GetClusterScopeCycleIndex() int { + return a.ClusterScopeCycleIndex +} + +func (a *HostAddress) SetClusterScopeCycleIndex(index int) { + a.ClusterScopeCycleIndex = index +} + +func (a *HostAddress) GetClusterScopeCycleOffset() int { + return a.ClusterScopeCycleOffset +} + +func (a *HostAddress) SetClusterScopeCycleOffset(offset int) { + a.ClusterScopeCycleOffset = offset +} + +// CompactString creates compact string representation +func (a HostAddress) CompactString() string { + return fmt.Sprintf("ns:%s|chi:%s|clu:%s|sha:%s|rep:%s|host:%s", + a.GetNamespace(), + a.GetCRName(), + a.GetClusterName(), + a.GetShardName(), + a.GetReplicaName(), + a.GetHostName()) +} + +// ClusterNameString creates cluster+host pair +func (a HostAddress) ClusterNameString() string { + return fmt.Sprintf("%s/%s", a.GetClusterName(), a.GetHostName()) +} + +// NamespaceNameString creates namespace+name pair +func (a HostAddress) NamespaceNameString() string { + return fmt.Sprintf("%s/%s", a.GetNamespace(), a.GetHostName()) +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_hosts_field.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_hosts_field.go new file mode 100644 index 00000000..e7fd9c0e --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_hosts_field.go @@ -0,0 +1,80 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// HostsField specifies field of hosts +type HostsField struct { + ShardsCount int + ReplicasCount int + Field [][]*Host +} + +// NewHostsField creates new field of hosts +func NewHostsField(shards, replicas int) *HostsField { + hf := new(HostsField) + + hf.ShardsCount = shards + hf.ReplicasCount = replicas + + hf.Field = make([][]*Host, hf.ShardsCount) + for shard := 0; shard < hf.ShardsCount; shard++ { + hf.Field[shard] = make([]*Host, hf.ReplicasCount) + } + + return hf +} + +// Set sets host on specified coordinates +func (hf *HostsField) Set(shard, replica int, host *Host) { + hf.Field[shard][replica] = host +} + +// Get gets host from specified coordinates +func (hf *HostsField) Get(shard, replica int) *Host { + return hf.Field[shard][replica] +} + +// GetOrCreate gets and creates if necessary +func (hf *HostsField) GetOrCreate(shard, replica int) *Host { + if hf.Field[shard][replica] == nil { + hf.Field[shard][replica] = new(Host) + } + return hf.Field[shard][replica] +} + +// WalkHosts walks hosts with a function +func (hf *HostsField) WalkHosts(f func(shard, replica int, host *Host) error) []error { + res := make([]error, 0) + + for shardIndex := range hf.Field { + for replicaIndex := range hf.Field[shardIndex] { + if host := hf.Get(shardIndex, replicaIndex); host != nil { + res = append(res, f(shardIndex, replicaIndex, host)) + } + } + } + + return res +} + +// HostsCount returns hosts number +func (hf *HostsField) HostsCount() int { + count := 0 + hf.WalkHosts(func(shard, replica int, host *Host) error { + count++ + return nil + }) + return count +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_reconcile.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_reconcile.go new file mode 100644 index 00000000..83ceb194 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_reconcile.go @@ -0,0 +1,277 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "strings" + "time" +) + +// ChiReconcile defines reconcile specification +type ChiReconcile struct { + // About to be DEPRECATED + Policy string `json:"policy,omitempty" yaml:"policy,omitempty"` + + // ConfigMapPropagationTimeout specifies timeout for ConfigMap to propagate + ConfigMapPropagationTimeout int `json:"configMapPropagationTimeout,omitempty" yaml:"configMapPropagationTimeout,omitempty"` + // Cleanup specifies cleanup behavior + Cleanup *Cleanup `json:"cleanup,omitempty" yaml:"cleanup,omitempty"` + // Macros specifies macros application rules + Macros ReconcileMacros `json:"macros,omitempty" yaml:"macros,omitempty"` + + // Runtime specifies runtime settings + Runtime ReconcileRuntime `json:"runtime,omitempty" yaml:"runtime,omitempty"` + // StatefulSet specifies StatefulSet reconcile settings + StatefulSet ReconcileStatefulSet `json:"statefulSet,omitempty" yaml:"statefulSet,omitempty"` + // Host specifies host-lever reconcile settings + Host ReconcileHost `json:"host" yaml:"host"` +} + +type ClusterReconcile struct { + // Runtime specifies runtime settings + Runtime ReconcileRuntime `json:"runtime" yaml:"runtime"` + // StatefulSet specifies StatefulSet reconcile settings + StatefulSet ReconcileStatefulSet `json:"statefulSet,omitempty" yaml:"statefulSet,omitempty"` + // Host specifies host-lever reconcile settings + Host ReconcileHost `json:"host" yaml:"host"` +} + +// ReconcileStatefulSet defines StatefulSet reconcile settings +type ReconcileStatefulSet struct { + Create ReconcileStatefulSetCreate `json:"create,omitempty" yaml:"create,omitempty"` + Update ReconcileStatefulSetUpdate `json:"update,omitempty" yaml:"update,omitempty"` + Recreate ReconcileStatefulSetRecreate `json:"recreate,omitempty" yaml:"recreate,omitempty"` +} + +// ReconcileStatefulSetCreate defines StatefulSet create settings +type ReconcileStatefulSetCreate struct { + OnFailure string `json:"onFailure,omitempty" yaml:"onFailure,omitempty"` +} + +// ReconcileStatefulSetUpdate defines StatefulSet update settings +type ReconcileStatefulSetUpdate struct { + Timeout uint64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` + PollInterval uint64 `json:"pollInterval,omitempty" yaml:"pollInterval,omitempty"` + OnFailure string `json:"onFailure,omitempty" yaml:"onFailure,omitempty"` +} + +// ReconcileStatefulSetRecreate defines StatefulSet recreate settings +type ReconcileStatefulSetRecreate struct { + OnDataLoss string `json:"onDataLoss,omitempty" yaml:"onDataLoss,omitempty"` + OnUpdateFailure string `json:"onUpdateFailure,omitempty" yaml:"onUpdateFailure,omitempty"` +} + +// MergeFrom merges from specified ReconcileStatefulSet +func (s ReconcileStatefulSet) MergeFrom(from ReconcileStatefulSet) ReconcileStatefulSet { + if s.Create.OnFailure == "" { + s.Create.OnFailure = from.Create.OnFailure + } + if s.Update.Timeout == 0 { + s.Update.Timeout = from.Update.Timeout + } + if s.Update.PollInterval == 0 { + s.Update.PollInterval = from.Update.PollInterval + } + if s.Update.OnFailure == "" { + s.Update.OnFailure = from.Update.OnFailure + } + if s.Recreate.OnDataLoss == "" { + s.Recreate.OnDataLoss = from.Recreate.OnDataLoss + } + if s.Recreate.OnUpdateFailure == "" { + s.Recreate.OnUpdateFailure = from.Recreate.OnUpdateFailure + } + return s +} + +func (reconcile *ClusterReconcile) Ensure() *ClusterReconcile { + if reconcile == nil { + reconcile = &ClusterReconcile{} + } + return reconcile +} + +// NewChiReconcile creates new reconcile +func NewChiReconcile() *ChiReconcile { + return new(ChiReconcile) +} + +// MergeFrom merges from specified reconcile +func (r *ChiReconcile) MergeFrom(from *ChiReconcile, _type MergeType) *ChiReconcile { + if from == nil { + return r + } + + if r == nil { + r = NewChiReconcile() + } + + switch _type { + case MergeTypeFillEmptyValues: + if r.Policy == "" { + r.Policy = from.Policy + } + if r.ConfigMapPropagationTimeout == 0 { + r.ConfigMapPropagationTimeout = from.ConfigMapPropagationTimeout + } + case MergeTypeOverrideByNonEmptyValues: + if from.Policy != "" { + // Override by non-empty values only + r.Policy = from.Policy + } + if from.ConfigMapPropagationTimeout != 0 { + // Override by non-empty values only + r.ConfigMapPropagationTimeout = from.ConfigMapPropagationTimeout + } + } + + r.Cleanup = r.Cleanup.MergeFrom(from.Cleanup, _type) + r.Macros = r.Macros.MergeFrom(from.Macros, _type) + r.Runtime = r.Runtime.MergeFrom(from.Runtime, _type) + r.StatefulSet = r.StatefulSet.MergeFrom(from.StatefulSet) + r.Host = r.Host.MergeFrom(from.Host) + + return r +} + +// SetDefaults set default values for reconcile +func (r *ChiReconcile) SetDefaults() *ChiReconcile { + if r == nil { + return nil + } + r.Policy = ReconcilingPolicyUnspecified + r.ConfigMapPropagationTimeout = 10 + r.Cleanup = NewCleanup().SetDefaults() + return r +} + +// GetPolicy gets policy +func (r *ChiReconcile) GetPolicy() string { + if r == nil { + return "" + } + return r.Policy +} + +// SetPolicy sets policy +func (r *ChiReconcile) SetPolicy(p string) { + if r == nil { + return + } + r.Policy = p +} + +func (r *ChiReconcile) HasConfigMapPropagationTimeout() bool { + return r.GetConfigMapPropagationTimeout() > 0 +} + +// GetConfigMapPropagationTimeout gets config map propagation timeout +func (r *ChiReconcile) GetConfigMapPropagationTimeout() int { + if r == nil { + return 0 + } + return r.ConfigMapPropagationTimeout +} + +// SetConfigMapPropagationTimeout sets config map propagation timeout +func (r *ChiReconcile) SetConfigMapPropagationTimeout(timeout int) { + if r == nil { + return + } + r.ConfigMapPropagationTimeout = timeout +} + +// GetConfigMapPropagationTimeoutDuration gets config map propagation timeout duration +func (r *ChiReconcile) GetConfigMapPropagationTimeoutDuration() time.Duration { + if r == nil { + return 0 + } + return time.Duration(r.GetConfigMapPropagationTimeout()) * time.Second +} + +// Possible reconcile policy values +const ( + ReconcilingPolicyUnspecified = "unspecified" + ReconcilingPolicyWait = "wait" + ReconcilingPolicyNoWait = "nowait" +) + +// IsReconcilingPolicyWait checks whether reconcile policy is "wait" +func (r *ChiReconcile) IsReconcilingPolicyWait() bool { + return strings.ToLower(r.GetPolicy()) == ReconcilingPolicyWait +} + +// IsReconcilingPolicyNoWait checks whether reconcile policy is "no wait" +func (r *ChiReconcile) IsReconcilingPolicyNoWait() bool { + return strings.ToLower(r.GetPolicy()) == ReconcilingPolicyNoWait +} + +// GetCleanup gets cleanup +func (r *ChiReconcile) GetCleanup() *Cleanup { + if r == nil { + return nil + } + return r.Cleanup +} + +// GetCleanup gets cleanup +func (r *ChiReconcile) SetCleanup(cleanup *Cleanup) { + if r == nil { + return + } + r.Cleanup = cleanup +} + +func (r *ChiReconcile) InheritRuntimeFrom(from OperatorConfigReconcileRuntime) { + if r == nil { + return + } + + if r.Runtime.ReconcileShardsThreadsNumber == 0 { + r.Runtime.ReconcileShardsThreadsNumber = from.ReconcileShardsThreadsNumber + } + if r.Runtime.ReconcileShardsMaxConcurrencyPercent == 0 { + r.Runtime.ReconcileShardsMaxConcurrencyPercent = from.ReconcileShardsMaxConcurrencyPercent + } +} + +func (r *ChiReconcile) InheritStatefulSetFrom(from OperatorConfigReconcile) { + if r == nil { + return + } + + if r.StatefulSet.Create.OnFailure == "" { + r.StatefulSet.Create.OnFailure = from.StatefulSet.Create.OnFailure + } + if r.StatefulSet.Update.Timeout == 0 { + r.StatefulSet.Update.Timeout = from.StatefulSet.Update.Timeout + } + if r.StatefulSet.Update.PollInterval == 0 { + r.StatefulSet.Update.PollInterval = from.StatefulSet.Update.PollInterval + } + if r.StatefulSet.Update.OnFailure == "" { + r.StatefulSet.Update.OnFailure = from.StatefulSet.Update.OnFailure + } + if r.StatefulSet.Recreate.OnDataLoss == "" { + r.StatefulSet.Recreate.OnDataLoss = from.StatefulSet.Recreate.OnDataLoss + } + if r.StatefulSet.Recreate.OnUpdateFailure == "" { + r.StatefulSet.Recreate.OnUpdateFailure = from.StatefulSet.Recreate.OnUpdateFailure + } +} + +func (r *ChiReconcile) InheritHostFrom(from ReconcileHost) { + r.Host = r.Host.MergeFrom(from) +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_reconcile_macros.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_reconcile_macros.go new file mode 100644 index 00000000..5400989d --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_reconcile_macros.go @@ -0,0 +1,57 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +type ReconcileMacros struct { + Sections MacrosSections `json:"sections,omitempty" yaml:"sections,omitempty"` +} + +// MergeFrom merges from specified reconcile +func (t ReconcileMacros) MergeFrom(from ReconcileMacros, _type MergeType) ReconcileMacros { + t.Sections = t.Sections.MergeFrom(from.Sections, _type) + return t +} + +type MacrosSections struct { + Users MacrosSection `json:"users,omitempty" yaml:"users,omitempty"` + Profiles MacrosSection `json:"profiles,omitempty" yaml:"profiles,omitempty"` + Quotas MacrosSection `json:"quotas,omitempty" yaml:"quotas,omitempty"` + Settings MacrosSection `json:"settings,omitempty" yaml:"settings,omitempty"` + Files MacrosSection `json:"files,omitempty" yaml:"files,omitempty"` +} + +// MergeFrom merges from specified macros +func (t MacrosSections) MergeFrom(from MacrosSections, _type MergeType) MacrosSections { + t.Users = t.Users.MergeFrom(from.Users, _type) + t.Profiles = t.Profiles.MergeFrom(from.Profiles, _type) + t.Quotas = t.Quotas.MergeFrom(from.Quotas, _type) + t.Settings = t.Settings.MergeFrom(from.Settings, _type) + t.Files = t.Files.MergeFrom(from.Files, _type) + return t +} + +type MacrosSection struct { + Enabled *types.StringBool `json:"enabled,omitempty" yaml:"enabled,omitempty"` +} + +// MergeFrom merges from specified macros +func (t MacrosSection) MergeFrom(from MacrosSection, _type MergeType) MacrosSection { + t.Enabled = t.Enabled.MergeFrom(from.Enabled) + return t +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_reconcile_runtime.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_reconcile_runtime.go new file mode 100644 index 00000000..f30dfcee --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_reconcile_runtime.go @@ -0,0 +1,42 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +type ReconcileRuntime struct { + ReconcileShardsThreadsNumber int `json:"reconcileShardsThreadsNumber,omitempty" yaml:"reconcileShardsThreadsNumber,omitempty"` + ReconcileShardsMaxConcurrencyPercent int `json:"reconcileShardsMaxConcurrencyPercent,omitempty" yaml:"reconcileShardsMaxConcurrencyPercent,omitempty"` +} + +func (r ReconcileRuntime) MergeFrom(from ReconcileRuntime, _type MergeType) ReconcileRuntime { + switch _type { + case MergeTypeFillEmptyValues: + if r.ReconcileShardsThreadsNumber == 0 { + r.ReconcileShardsThreadsNumber = from.ReconcileShardsThreadsNumber + } + if r.ReconcileShardsMaxConcurrencyPercent == 0 { + r.ReconcileShardsMaxConcurrencyPercent = from.ReconcileShardsMaxConcurrencyPercent + } + case MergeTypeOverrideByNonEmptyValues: + if from.ReconcileShardsThreadsNumber != 0 { + // Override by non-empty values only + r.ReconcileShardsThreadsNumber = from.ReconcileShardsThreadsNumber + } + if from.ReconcileShardsMaxConcurrencyPercent != 0 { + // Override by non-empty values only + r.ReconcileShardsMaxConcurrencyPercent = from.ReconcileShardsMaxConcurrencyPercent + } + } + return r +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_replica.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_replica.go new file mode 100644 index 00000000..d358a0a2 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_replica.go @@ -0,0 +1,200 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// ChiReplica defines item of a replica section of .spec.configuration.clusters[n].replicas +// TODO unify with ChiShard based on HostsSet +type ChiReplica struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Settings *Settings `json:"settings,omitempty" yaml:"settings,omitempty"` + Files *Settings `json:"files,omitempty" yaml:"files,omitempty"` + Templates *TemplatesList `json:"templates,omitempty" yaml:"templates,omitempty"` + ShardsCount int `json:"shardsCount,omitempty" yaml:"shardsCount,omitempty"` + // TODO refactor into map[string]Host + Hosts []*Host `json:"shards,omitempty" yaml:"shards,omitempty"` + + Runtime ChiReplicaRuntime `json:"-" yaml:"-"` +} + +type ChiReplicaRuntime struct { + Address ChiReplicaAddress `json:"-" yaml:"-"` + CHI *ClickHouseInstallation `json:"-" yaml:"-" testdiff:"ignore"` +} + +func (r *ChiReplicaRuntime) GetAddress() IReplicaAddress { + return &r.Address +} + +func (r *ChiReplicaRuntime) SetCR(cr ICustomResource) { + r.CHI = cr.(*ClickHouseInstallation) +} + +func (replica *ChiReplica) GetName() string { + return replica.Name +} + +// InheritSettingsFrom inherits settings from specified cluster +func (replica *ChiReplica) InheritSettingsFrom(cluster *Cluster) { + replica.Settings = replica.Settings.MergeFrom(cluster.Settings) +} + +// InheritFilesFrom inherits files from specified cluster +func (replica *ChiReplica) InheritFilesFrom(cluster *Cluster) { + replica.Files = replica.Files.MergeFrom(cluster.Files) +} + +// InheritTemplatesFrom inherits templates from specified cluster +func (replica *ChiReplica) InheritTemplatesFrom(cluster *Cluster) { + replica.Templates = replica.Templates.MergeFrom(cluster.Templates, MergeTypeFillEmptyValues) + replica.Templates.HandleDeprecatedFields() +} + +// GetServiceTemplate gets service template +func (replica *ChiReplica) GetServiceTemplate() (*ServiceTemplate, bool) { + if !replica.Templates.HasReplicaServiceTemplate() { + return nil, false + } + name := replica.Templates.GetReplicaServiceTemplate() + return replica.Runtime.CHI.GetServiceTemplate(name) +} + +// HasShardsCount checks whether replica has shards count specified +func (replica *ChiReplica) HasShardsCount() bool { + if replica == nil { + return false + } + + return replica.ShardsCount > 0 +} + +// WalkHosts walks over hosts +func (replica *ChiReplica) WalkHosts(f func(host *Host) error) []error { + res := make([]error, 0) + + for shardIndex := range replica.Hosts { + host := replica.Hosts[shardIndex] + res = append(res, f(host)) + } + + return res +} + +// HostsCount returns number of hosts +func (replica *ChiReplica) HostsCount() int { + count := 0 + replica.WalkHosts(func(host *Host) error { + count++ + return nil + }) + return count +} + +func (replica *ChiReplica) HasSettings() bool { + return replica.GetSettings() != nil +} + +func (replica *ChiReplica) GetSettings() *Settings { + if replica == nil { + return nil + } + return replica.Settings +} + +func (replica *ChiReplica) HasFiles() bool { + return replica.GetFiles() != nil +} + +func (replica *ChiReplica) GetFiles() *Settings { + if replica == nil { + return nil + } + return replica.Files +} + +func (replica *ChiReplica) HasTemplates() bool { + return replica.GetTemplates() != nil +} + +func (replica *ChiReplica) GetTemplates() *TemplatesList { + if replica == nil { + return nil + } + return replica.Templates +} + +func (replica *ChiReplica) GetRuntime() IReplicaRuntime { + if replica == nil { + return (*ChiReplicaRuntime)(nil) + } + return &replica.Runtime +} + +// ChiReplicaAddress defines address of a replica within ClickHouseInstallation +type ChiReplicaAddress struct { + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + CHIName string `json:"chiName,omitempty" yaml:"chiName,omitempty"` + ClusterName string `json:"clusterName,omitempty" yaml:"clusterName,omitempty"` + ClusterIndex int `json:"clusterIndex,omitempty" yaml:"clusterIndex,omitempty"` + ReplicaName string `json:"replicaName,omitempty" yaml:"replicaName,omitempty"` + ReplicaIndex int `json:"replicaIndex,omitempty" yaml:"replicaIndex,omitempty"` +} + +func (a *ChiReplicaAddress) GetNamespace() string { + return a.Namespace +} + +func (a *ChiReplicaAddress) SetNamespace(namespace string) { + a.Namespace = namespace +} + +func (a *ChiReplicaAddress) GetCRName() string { + return a.CHIName +} + +func (a *ChiReplicaAddress) SetCRName(name string) { + a.CHIName = name +} + +func (a *ChiReplicaAddress) GetClusterName() string { + return a.ClusterName +} + +func (a *ChiReplicaAddress) SetClusterName(name string) { + a.ClusterName = name +} + +func (a *ChiReplicaAddress) GetClusterIndex() int { + return a.ClusterIndex +} + +func (a *ChiReplicaAddress) SetClusterIndex(index int) { + a.ClusterIndex = index +} + +func (a *ChiReplicaAddress) GetReplicaName() string { + return a.ReplicaName +} + +func (a *ChiReplicaAddress) SetReplicaName(name string) { + a.ReplicaName = name +} + +func (a *ChiReplicaAddress) GetReplicaIndex() int { + return a.ReplicaIndex +} + +func (a *ChiReplicaAddress) SetReplicaIndex(index int) { + a.ReplicaIndex = index +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting.go new file mode 100644 index 00000000..1bade8a9 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting.go @@ -0,0 +1,425 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" +) + +// Settings value can be one of: +// 1. scalar value (string, int, bool, etc). +// Ex.: +// user1/networks/ip: "::/0" +// 2. vector of scalars +// Ex.: +// user1/networks/ip: +// - "127.0.0.1" +// - "192.168.1.2" +// We do not know types of these scalars in advance also + +// Setting represents one settings, which can be either a sting or a vector of strings +type Setting struct { + _type SettingType + scalar string + vector []string + src *SettingSource + attributes map[string]string + embed bool +} + +type SettingType string + +// List of possible setting types +const ( + SettingTypeUnknown SettingType = "unknown" + SettingTypeScalar SettingType = "scalar" + SettingTypeVector SettingType = "vector" + SettingTypeSource SettingType = "source" +) + +// Ensure required interface implementation +var ( + // unimplemented + //_ yaml.Marshaler = &Setting{} + //_ yaml.Unmarshaler = &Setting{} + _ json.Marshaler = &Setting{} + _ json.Unmarshaler = &Setting{} +) + +// List of returned errors +var ( + ErrDataSourceAddressHasIncorrectFormat = fmt.Errorf("data source address has incorrect format") + ErrUnableToUnmarshal = fmt.Errorf("unable to unmarshal setting") + ErrUnableToUnmarshalIntoNil = fmt.Errorf("unable to unmarshal into nil") +) + +// NewSettingFromAny builds new setting from either of: +// 1. scalar +// 2. vector +// 3. source +// In case of being unable to build new setting an error is returned +func NewSettingFromAny(untyped any) (*Setting, error) { + if scalarSetting, ok := NewSettingScalarFromAny(untyped); ok && scalarSetting.HasValue() { + return scalarSetting, nil + } + + if vectorSetting, ok := NewSettingVectorFromAny(untyped); ok && vectorSetting.HasValue() { + return vectorSetting, nil + } + + if srcSetting, ok := NewSettingSourceFromAny(untyped); ok && srcSetting.HasValue() { + return srcSetting, nil + } + + return nil, ErrUnableToUnmarshal +} + +// IsEmpty checks whether settings ia an empty one +func (s *Setting) IsEmpty() bool { + return s == nil +} + +// AsAny gets value of a setting as untyped +func (s *Setting) AsAny() any { + if s == nil { + return nil + } + switch s.Type() { + case SettingTypeScalar: + return s.scalarAsAny() + case SettingTypeVector: + return s.vectorAsAny() + case SettingTypeSource: + return s.sourceAsAny() + } + return nil +} + +// Type gets type of the setting +func (s *Setting) Type() SettingType { + if s == nil { + return SettingTypeUnknown + } + return s._type +} + +// SetAttribute sets attribute of the setting +func (s *Setting) SetAttribute(name, value string) *Setting { + if s == nil { + return nil + } + if s.attributes == nil { + s.attributes = make(map[string]string) + } + s.attributes[name] = value + return s +} + +// HasAttribute checks whether setting has specified attribute +func (s *Setting) HasAttribute(name string) bool { + if s == nil { + return false + } + if s.attributes == nil { + return false + } + _, ok := s.attributes[name] + return ok +} + +// HasAttributes checks whether setting has attributes +func (s *Setting) HasAttributes() bool { + if s == nil { + return false + } + return len(s.attributes) > 0 +} + +// Attributes returns string form of attributes - used to config tag creation +func (s *Setting) Attributes() string { + if s == nil { + return "" + } + a := "" + for name, value := range s.attributes { + a += fmt.Sprintf(` %s="%s"`, name, value) + } + return a +} + +// Len returns number of entries in the Setting (be it a scalar or a vector) +func (s *Setting) Len() int { + switch s.Type() { + case SettingTypeScalar: + return 1 + case SettingTypeVector: + return len(s.vector) + case SettingTypeSource: + return 1 + default: + return 0 + } +} + +// HasValue checks whether setting has non zero-value (some value) +func (s *Setting) HasValue() bool { + switch s.Type() { + case SettingTypeScalar: + return s.Len() > 0 + case SettingTypeVector: + return s.Len() > 0 + case SettingTypeSource: + return s.src.HasValue() + default: + return false + } +} + +// ApplyMacros applies macros on the Setting +func (s *Setting) ApplyMacros(macros *util.Replacer) { + if s == nil { + return + } + + switch s.Type() { + case SettingTypeScalar: + s.scalar = macros.Line(s.scalar) + case SettingTypeVector: + s.vector = macros.Slice(s.vector) + case SettingTypeSource: + // Unimplemented + } +} + +// MergeFrom merges from specified source +func (s *Setting) MergeFrom(from *Setting) *Setting { + // Need to have something to merge from + if from == nil { + return s + } + + // Can merge from Vector only + from = from.CastToVector() + + // Reasonable to merge values only + if !from.HasValue() { + return s + } + + // In case recipient does not exist just copy values from source + if s == nil { + new := NewSettingVector(from.VectorOfStrings()) + new.attributes = util.MergeStringMapsPreserve(new.attributes, from.attributes) + return new + } + + s.vector = util.MergeStringArrays(s.vector, from.vector) + s.attributes = util.MergeStringMapsPreserve(s.attributes, from.attributes) + + return s +} + +// String gets string value of a setting. Vector is combined into one string +func (s *Setting) String() string { + if s == nil { + return "" + } + + switch s.Type() { + case SettingTypeScalar: + return s.ScalarString() + case SettingTypeVector: + return "[" + strings.Join(s.vector, ", ") + "]" + case SettingTypeSource: + return "data source" + } + + return "" +} + +// String gets string value of a setting. Vector is combined into one string +func (s *Setting) StringFull() string { + if s == nil { + return "" + } + + attributes := "" + if s.HasAttributes() { + attributes = ":[" + s.Attributes() + "]" + } + + return s.String() + attributes +} + +// CastToVector returns either Setting in case it is vector or newly created Setting with value casted to VectorOfStrings +func (s *Setting) CastToVector() *Setting { + if s == nil { + return nil + } + switch s.Type() { + case SettingTypeScalar: + return NewSettingVector(s.AsVectorOfStrings()) + } + return s +} + +// FetchDataSourceAddress fetches data source address from the setting. +// defaultNamespace specifies default namespace to be used in case there is no namespace specified in data source address. +func (s *Setting) FetchDataSourceAddress(defaultNamespace string, parseScalarString bool) (types.ObjectAddress, error) { + switch s.Type() { + case SettingTypeScalar: + if parseScalarString { + // Fetch k8s address of the field from the string + return s.parseDataSourceAddress(s.String(), defaultNamespace) + } + case SettingTypeSource: + // Fetch k8s address of the field from the source ref + // 1. The name of the secret to select from. Namespace is expected to be provided externally + // 2. The key of the secret to select from. + name, key := s.GetNameKey() + return types.ObjectAddress{ + Namespace: defaultNamespace, + Name: name, + Key: key, + }, nil + } + + return types.ObjectAddress{}, fmt.Errorf("%w - unknown setting type", ErrDataSourceAddressHasIncorrectFormat) +} + +// parseDataSourceAddress parses address into namespace, name, key triple +func (s *Setting) parseDataSourceAddress(dataSourceAddress, defaultNamespace string) (addr types.ObjectAddress, err error) { + // Extract data source's namespace and name and then field name within the data source, + // by splitting namespace/name/field (aka key) triple. Namespace can be omitted though + switch tags := strings.Split(dataSourceAddress, "/"); len(tags) { + case 3: + // All components are in place. Expect to have namespace/name/key triple + addr.Namespace = tags[0] + addr.Name = tags[1] + addr.Key = tags[2] + case 2: + // Assume namespace is omitted. Expect to have name/key pair + addr.Namespace = defaultNamespace + addr.Name = tags[0] + addr.Key = tags[1] + default: + // Skip incorrect entry + return types.ObjectAddress{}, fmt.Errorf("%w, dataSourceAddress: %s", ErrDataSourceAddressHasIncorrectFormat, dataSourceAddress) + } + + // Sanity check for all address components being in place + if addr.AnyEmpty() { + return types.ObjectAddress{}, fmt.Errorf( + "%w, %s/%s/%s", + ErrDataSourceAddressHasIncorrectFormat, + addr.Namespace, addr.Name, addr.Key, + ) + } + + return addr, nil +} + +func (s *Setting) SetEmbed() *Setting { + if s == nil { + return nil + } + s.embed = true + return s +} + +func (s *Setting) IsEmbed() bool { + if s == nil { + return false + } + return s.embed +} + +// UnmarshalJSON unmarshal JSON +func (s *Setting) UnmarshalJSON(data []byte) error { + return s.Unmarshal(data, json.Unmarshal) +} + +// MarshalJSON marshals JSON +func (s *Setting) MarshalJSON() ([]byte, error) { + return s.marshal(json.Marshal) +} + +//// UnmarshalYAML unmarshal YAML +//func (s *Setting) UnmarshalYAML(data []byte) error { +// return s.Unmarshal(data, yaml.Unmarshal) +//} +// +//// MarshalYAML marshals YAML +//func (s *Setting) MarshalYAML() ([]byte, error) { +// return s.marshal(yaml.Marshal) +//} + +// Unmarshal +func (s *Setting) Unmarshal(data []byte, unmarshaller func(data []byte, v any) error) error { + if s == nil { + return ErrUnableToUnmarshalIntoNil + } + + // Prepare untyped map at first + var untyped any + + // Provided binary data is expected to unmarshal into untyped map, because settings are map-like struct + if err := unmarshaller(data, &untyped); err != nil { + return err + } + + // Build setting from untyped var + setting, err := NewSettingFromAny(untyped) + if err != nil { + return err + } + + // Copy typed value + *s = *setting + return nil +} + +// marshal +func (s *Setting) marshal(marshaller func(v any) ([]byte, error)) ([]byte, error) { + return marshaller(s.AsAny()) +} + +// Clone make clone of a setting +func (s *Setting) Clone() *Setting { + if s == nil { + return nil + } + + // Build json bytes + bytes, err := s.MarshalJSON() + if err != nil { + return nil + } + + // Fill clone with unmarshalled value + clone := &Setting{} + err = clone.UnmarshalJSON(bytes) + if err != nil { + return nil + } + + // Clone is ready + return clone +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting_data_source.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting_data_source.go new file mode 100644 index 00000000..64392222 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting_data_source.go @@ -0,0 +1,145 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "encoding/json" + + core "k8s.io/api/core/v1" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +// SettingSource defines setting as a ref to some data source +type SettingSource struct { + ValueFrom *types.DataSource `json:"valueFrom,omitempty" yaml:"valueFrom,omitempty"` +} + +// NewSettingSource makes new source Setting +func NewSettingSource(src *SettingSource) *Setting { + return &Setting{ + _type: SettingTypeSource, + src: src, + } +} + +// NewSettingSourceFromAny makes new source Setting from untyped +func NewSettingSourceFromAny(untyped any) (*Setting, bool) { + if srcValue, ok := parseSettingSourceValue(untyped); ok { + return NewSettingSource(srcValue), true + } + + return nil, false +} + +// GetNameKey gets name and key from the secret ref +// 1. The name of the secret to select from. Namespace is expected to be provided externally +// 2. The key of the secret to select from. +func (s *SettingSource) GetNameKey() (string, string) { + if ref := s.GetSecretKeyRef(); ref != nil { + return ref.Name, ref.Key + } + return "", "" +} + +// GetSecretKeyRef gets SecretKeySelector (typically named as SecretKeyRef) or nil +func (s *SettingSource) GetSecretKeyRef() *core.SecretKeySelector { + if s == nil { + return nil + } + if s.ValueFrom == nil { + return nil + } + return s.ValueFrom.SecretKeyRef +} + +// HasSecretKeyRef checks whether SecretKeySelector (typically named as SecretKeyRef) is available +func (s *SettingSource) HasSecretKeyRef() bool { + return s.GetSecretKeyRef() != nil +} + +// HasValue checks whether SettingSource has no value +func (s *SettingSource) HasValue() bool { + if s == nil { + return false + } + if s.ValueFrom == nil { + return false + } + return s.HasSecretKeyRef() +} + +// sourceAsAny gets source value of a setting as any +func (s *Setting) sourceAsAny() any { + if s == nil { + return nil + } + + return s.src +} + +// IsSource checks whether setting is a source value +func (s *Setting) IsSource() bool { + return s.Type() == SettingTypeSource +} + +// GetNameKey gets name and key of source setting +// 1. The name of the secret to select from. Namespace is expected to be provided externally +// 2. The key of the secret to select from. +func (s *Setting) GetNameKey() (string, string) { + if ref := s.GetSecretKeyRef(); ref != nil { + return ref.Name, ref.Key + } + return "", "" +} + +// GetSecretKeyRef gets SecretKeySelector (typically named as SecretKeyRef) or nil +func (s *Setting) GetSecretKeyRef() *core.SecretKeySelector { + if s == nil { + return nil + } + if !s.IsSource() { + return nil + } + + return s.src.GetSecretKeyRef() +} + +// HasSecretKeyRef checks whether SecretKeySelector (typically named as SecretKeyRef) is available +func (s *Setting) HasSecretKeyRef() bool { + if s == nil { + return false + } + if !s.IsSource() { + return false + } + + return s.GetSecretKeyRef() != nil +} + +func parseSettingSourceValue(untyped any) (*SettingSource, bool) { + jsonStr, err := json.Marshal(untyped) + if err != nil { + return nil, false + } + + // Convert json string to struct + var settingSource SettingSource + if err := json.Unmarshal(jsonStr, &settingSource); err != nil { + return nil, false + } + + return &settingSource, true +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting_scalar.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting_scalar.go new file mode 100644 index 00000000..683941d7 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting_scalar.go @@ -0,0 +1,154 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "fmt" + "math" + "reflect" + "strconv" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +// NewSettingScalar makes new scalar Setting +func NewSettingScalar(scalar string) *Setting { + return &Setting{ + _type: SettingTypeScalar, + scalar: scalar, + } +} + +// NewSettingScalarFromAny makes new scalar Setting from untyped +func NewSettingScalarFromAny(untyped any) (*Setting, bool) { + if scalar, ok := parseSettingScalarValue(untyped); ok { + return NewSettingScalar(scalar), true + } + + return nil, false +} + +// MustNewSettingScalarFromAny makes new scalar Setting from untyped +func MustNewSettingScalarFromAny(untyped any) *Setting { + if scalar, ok := parseSettingScalarValue(untyped); ok { + return NewSettingScalar(scalar) + } + + return nil +} + +const ( + // Float with fractional part less than ignoreThreshold is considered to be int and is casted to int + ignoreThreshold = 0.001 +) + +func parseSettingScalarValue(untyped any) (string, bool) { + typeOf := reflect.TypeOf(untyped) + if typeOf == nil { + // Unable to determine type of the value + return "", false + } + + switch untyped.(type) { + case fmt.Stringer: + stringer := untyped.(fmt.Stringer) + return fmt.Sprintf("%s", stringer), true + case // scalar + int, uint, + int8, uint8, + int16, uint16, + int32, uint32, + int64, uint64, + bool, + string: + return fmt.Sprintf("%v", untyped), true + case // scalar + float32: + floatVal := untyped.(float32) + // What is the fractional part of the float value? + // If it is too small, we can consider the value to be an int value + _, frac := math.Modf(float64(floatVal)) + if frac > ignoreThreshold { + // Consider it float + return fmt.Sprintf("%f", untyped), true + } else { + // Consider it int + intVal := int64(floatVal) + return fmt.Sprintf("%v", intVal), true + } + case // scalar + float64: + floatVal := untyped.(float64) + // What is the fractional part of the float value? + // If it is too small, we can consider the value to be an int value + _, frac := math.Modf(floatVal) + if frac > ignoreThreshold { + // Consider it float + return fmt.Sprintf("%f", untyped), true + } else { + // Consider it int + intVal := int64(floatVal) + return fmt.Sprintf("%v", intVal), true + } + } + + return "", false +} + +// IsScalar checks whether setting is a scalar value +func (s *Setting) IsScalar() bool { + return s.Type() == SettingTypeScalar +} + +// ScalarString gets string scalar value of a setting +func (s *Setting) ScalarString() string { + if s == nil { + return "" + } + return s.scalar +} + +// ScalarInt gets int scalar value of a setting +func (s *Setting) ScalarInt() int { + if s == nil { + return 0 + } + if value, err := strconv.Atoi(s.scalar); err == nil { + return value + } + + return 0 +} + +// ScalarInt gets int scalar value of a setting +func (s *Setting) ScalarInt32Ptr() *types.Int32 { + if s == nil { + return nil + } + if value, err := strconv.Atoi(s.scalar); err == nil { + return types.NewInt32(int32(value)) + } + + return nil +} + +// scalarAsAny gets scalar value of a setting as any +func (s *Setting) scalarAsAny() any { + if s == nil { + return nil + } + + return s.scalar +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting_vector.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting_vector.go new file mode 100644 index 00000000..56da86f9 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_setting_vector.go @@ -0,0 +1,92 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "reflect" +) + +// NewSettingVector makes new vector Setting +func NewSettingVector(vector []string) *Setting { + return &Setting{ + _type: SettingTypeVector, + vector: vector, + } +} + +// NewSettingVectorFromAny makes new vector Setting from untyped +func NewSettingVectorFromAny(untyped any) (*Setting, bool) { + if vector, ok := parseSettingVectorValue(untyped); ok { + return NewSettingVector(vector), true + } + + return nil, false +} + +func parseSettingVectorValue(untyped any) ([]string, bool) { + var vectorValue []string + + typeOf := reflect.TypeOf(untyped) + if typeOf == nil { + // Unable to determine type of the value + return nil, false + } + + if vector, ok := untyped.([]any); ok { + for _, possibleScalar := range vector { + if scalarValue, ok := parseSettingScalarValue(possibleScalar); ok { + vectorValue = append(vectorValue, scalarValue) + } + } + return vectorValue, true + } + + return nil, false +} + +// IsVector checks whether setting is a vector value +func (s *Setting) IsVector() bool { + return s.Type() == SettingTypeVector +} + +// VectorOfStrings gets vector values of a setting +func (s *Setting) VectorOfStrings() []string { + if s == nil { + return nil + } + return s.vector +} + +// vectorAsAny gets vector value of a setting as any +func (s *Setting) vectorAsAny() any { + if s == nil { + return nil + } + + return s.vector +} + +// AsVectorOfStrings gets value of a setting as vector. ScalarString value is casted to vector +func (s *Setting) AsVectorOfStrings() []string { + if s == nil { + return nil + } + if s.IsScalar() { + return []string{ + s.ScalarString(), + } + } + return s.vector +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings.go new file mode 100644 index 00000000..142fbff9 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings.go @@ -0,0 +1,857 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "bytes" + "encoding/json" + "fmt" + "regexp" + "sort" + "strings" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/xml" + "gopkg.in/d4l3k/messagediff.v1" +) + +// Specify returned errors for being re-used +var ( + errorNoSectionSpecified = fmt.Errorf("no section specified") + errorNoPrefixSpecified = fmt.Errorf("no prefix specified") + errorNoSuffixSpecified = fmt.Errorf("no suffix specified") +) + +// SettingsName2KeyConverter is an interface to describe different converters. +// Implements 'Strategy' pattern. +type SettingsName2KeyConverter interface { + // Name2Key converts name to storage key. This is the opposite to Key2Name + Name2Key(name string) string + // Key2Name converts storage key to name. This is the opposite to Name2Key + Key2Name(key string) string + // DeepCopySettingsName2KeyConverter is required for code auto-generator + DeepCopySettingsName2KeyConverter() SettingsName2KeyConverter +} + +// Settings specifies settings +type Settings struct { + // data is a data storage + data map[string]*Setting + // converter is an interface to describe different converters. + // Implements 'Strategy' pattern. + converter SettingsName2KeyConverter `json:"-" yaml:"-" testdiff:"ignore"` +} + +var ( + // unimplemented + //_ yaml.Marshaler = &Settings{} + //_ yaml.Unmarshaler = &Settings{} + _ json.Marshaler = &Settings{} + _ json.Unmarshaler = &Settings{} +) + +// NewSettings creates new settings +func NewSettings() *Settings { + s := &Settings{} + s.ensureDataStorage() + return s +} + +func NewSettingsScalarFromMap(m map[string]string) *Settings { + return NewSettings().SetScalarsFromMap(m) +} + +// ensureDataStorage ensures all internals of the structure are in place +func (s *Settings) ensureDataStorage() { + if s == nil { + return + } + if s.data == nil { + s.data = make(map[string]*Setting) + } +} + +// ensureConverter ensures converter in place +func (s *Settings) ensureConverter() SettingsName2KeyConverter { + if s == nil { + // Just return converter + return NewSettingsName2KeyConverterPlain() + } + if !s.HasConverter() { + s.SetConverter(NewSettingsName2KeyConverterPlain()) + } + return s.GetConverter() +} + +func (s *Settings) SetConverter(converter SettingsName2KeyConverter) *Settings { + if s != nil { + s.converter = converter + } + return s +} + +func (s *Settings) GetConverter() SettingsName2KeyConverter { + if s == nil { + return nil + } + return s.converter +} + +func (s *Settings) HasConverter() bool { + return s.GetConverter() != nil +} + +// Ensure ensures settings are in place +func (s *Settings) Ensure() *Settings { + if s == nil { + return NewSettings() + } + return s +} + +// Name2Key converts name to storage key. This is the opposite to Key2Name +func (s *Settings) Name2Key(name string) string { + return s.ensureConverter().Name2Key(name) +} + +// Key2Name converts storage key to name. This is the opposite to Name2Key +func (s *Settings) Key2Name(key string) string { + return s.ensureConverter().Key2Name(key) +} + +// Len gets length of the settings +func (s *Settings) Len() int { + if s == nil { + return 0 + } + return len(s.data) +} + +// WalkKeys walks over settings with a function. Function receives key and setting. +func (s *Settings) WalkKeys(f func(key string, setting *Setting)) { + if s == nil { + return + } + if s.Len() == 0 { + return + } + // Walk storage keys + for key := range s.data { + f(key, s.GetKey(key)) + } +} + +// WalkKeysSafe walks over settings with a function. Function receives key and setting. +func (s *Settings) WalkKeysSafe(f func(key string, setting *Setting)) { + if s == nil { + return + } + if s.Len() == 0 { + return + } + // Walk storage keys + keys := s.Keys() + for _, key := range keys { + if s.HasKey(key) { + f(key, s.GetKey(key)) + } + } +} + +// Walk walks over settings with a function. Function receives name and setting. +// Storage key is used internally. +func (s *Settings) Walk(f func(name string, setting *Setting)) { + s.WalkKeys(func(key string, _setting *Setting) { + f(s.Key2Name(key), _setting) + }) +} + +// WalkSafe walks over settings with a function. Function receives name and setting. +// Storage key is used internally. +func (s *Settings) WalkSafe(f func(name string, setting *Setting)) { + s.WalkKeysSafe(func(key string, _setting *Setting) { + f(s.Key2Name(key), _setting) + }) +} + +// WalkNames walks over settings with a function. Function receives name. +// Storage key is used internally. +func (s *Settings) WalkNames(f func(name string)) { + s.WalkKeys(func(key string, _setting *Setting) { + f(s.Key2Name(key)) + }) +} + +// HasKey checks whether key setting exists. +func (s *Settings) HasKey(key string) bool { + if s == nil { + return false + } + if s.Len() == 0 { + return false + } + // Check storage key exists + _, ok := s.data[key] + return ok +} + +// Has checks whether named setting exists. +// Storage key is used internally. +func (s *Settings) Has(name string) bool { + return s.HasKey(s.Name2Key(name)) +} + +// GetKey gets key setting. +func (s *Settings) GetKey(key string) *Setting { + if s == nil { + return nil + } + if s.Len() == 0 { + return nil + } + // get value by storage key + return s.data[key] +} + +// Get gets named setting. +// Storage key is used internally. +func (s *Settings) Get(name string) *Setting { + return s.GetKey(s.Name2Key(name)) +} + +// GetA gets named setting. +// Storage key is used internally. +func (s *Settings) GetA(name string) any { + return s.GetKey(s.Name2Key(name)) +} + +// SetKey sets key setting. +func (s *Settings) SetKey(key string, setting *Setting) *Settings { + if s == nil { + return s + } + s.ensureDataStorage() + // Set with storage key + s.data[key] = setting + return s +} + +// Set sets named setting. +// Storage key is used internally. +func (s *Settings) Set(name string, setting *Setting) *Settings { + return s.SetKey(s.Name2Key(name), setting) +} + +// DeleteKey deletes key setting +func (s *Settings) DeleteKey(key string) { + if s == nil { + return + } + if !s.HasKey(key) { + return + } + // Delete storage key + delete(s.data, key) +} + +// Delete deletes named setting +func (s *Settings) Delete(name string) { + s.DeleteKey(s.Name2Key(name)) +} + +// IsZero checks whether settings is zero +func (s *Settings) IsZero() bool { + if s == nil { + return true + } + return s.Len() == 0 +} + +// SetIfNotExists sets named setting +func (s *Settings) SetIfNotExists(name string, setting *Setting) *Settings { + if s == nil { + return s + } + if !s.Has(name) { + s.Set(name, setting) + } + return s +} + +// SetScalarsFromMap sets multiple scalars from map +func (s *Settings) SetScalarsFromMap(m map[string]string) *Settings { + // Copy values from the map + for name, value := range m { + s.Set(name, NewSettingScalar(value)) + } + return s +} + +// Keys gets keys of the settings in alphabetical order +func (s *Settings) Keys() (keys []string) { + s.WalkKeys(func(key string, setting *Setting) { + keys = append(keys, key) + }) + + // Sort keys to ensure deterministic ordering for Kubernetes manifest stability. + // Consistent ordering prevents unnecessary resource updates during reconciliation. + sort.Strings(keys) + return keys +} + +// Names gets names of the settings +func (s *Settings) Names() (names []string) { + s.Walk(func(name string, setting *Setting) { + names = append(names, name) + }) + return names +} + +// Prefixes gets prefixes of the settings +func (s *Settings) Prefixes(unique bool) (prefixes []string) { + s.WalkKeys(func(key string, setting *Setting) { + if prefix, err := getPrefixFromPath(key); err == nil { + prefixes = append(prefixes, prefix) + } + }) + + if unique { + prefixes = util.Unique(prefixes) + } + + sort.Strings(prefixes) + return prefixes +} + +// Groups gets groups of the settings +func (s *Settings) Groups() []string { + return s.Prefixes(true) +} + +// UnmarshalJSON unmarshal JSON +func (s *Settings) UnmarshalJSON(data []byte) error { + return s.unmarshal(data, json.Unmarshal) +} + +// MarshalJSON marshals JSON +func (s *Settings) MarshalJSON() ([]byte, error) { + return s.marshal(json.Marshal) +} + +//// UnmarshalYAML unmarshal YAML +//func (s *Settings) UnmarshalYAML(data []byte) error { +// return s.unmarshal(data, yaml.Unmarshal) +//} + +//// MarshalYAML marshals YAML +//func (s *Settings) MarshalYAML() ([]byte, error) { +// return s.marshal(yaml.Marshal) +//} + +// unmarshal +func (s *Settings) unmarshal(data []byte, unmarshaller func(data []byte, v any) error) error { + if s == nil { + return fmt.Errorf("unable to unmarshal with nil") + } + + // Prepare untyped map at first + type untypedMapType map[string]any + var untypedMap untypedMapType + + // Provided binary data is expected to unmarshal into untyped map, because settings are map-like struct + if err := unmarshaller(data, &untypedMap); err != nil { + return err + } + + // Entries are expected to exist + if len(untypedMap) == 0 { + return nil + } + + // Create entries from untyped map in result settings + for key, untyped := range untypedMap { + if setting, err := NewSettingFromAny(untyped); err == nil { + s.SetKey(key, setting) + } else { + // Unknown type of entry in untyped map + // Should error be reported? + // Skip for now + } + } + + return nil +} + +// marshal +func (s *Settings) marshal(marshaller func(v any) ([]byte, error)) ([]byte, error) { + if s == nil { + return marshaller(nil) + } + + raw := make(map[string]any) + s.WalkKeys(func(key string, setting *Setting) { + raw[key] = setting.AsAny() + }) + + return marshaller(raw) +} + +// getInt32Ptr gets Int32 pointer value +func (s *Settings) getInt32Ptr(name string) *types.Int32 { + return s.Get(name).ScalarInt32Ptr() +} + +// GetTCPPort gets TCP port from settings +func (s *Settings) GetTCPPort() *types.Int32 { + return s.getInt32Ptr("tcp_port") +} + +// GetTCPPortSecure gets TCP port secure from settings +func (s *Settings) GetTCPPortSecure() *types.Int32 { + return s.getInt32Ptr("tcp_port_secure") +} + +// GetHTTPPort gets HTTP port from settings +func (s *Settings) GetHTTPPort() *types.Int32 { + return s.getInt32Ptr("http_port") +} + +// GetHTTPSPort gets HTTPS port from settings +func (s *Settings) GetHTTPSPort() *types.Int32 { + return s.getInt32Ptr("https_port") +} + +// GetInterserverHTTPPort gets interserver HTTP port from settings +func (s *Settings) GetInterserverHTTPPort() *types.Int32 { + return s.getInt32Ptr("interserver_http_port") +} + +// GetZKPort gets Zookeeper port from settings +func (s *Settings) GetZKPort() *types.Int32 { + return s.getInt32Ptr("keeper_server/tcp_port") +} + +// GetRaftPort gets Raft port from settings +func (s *Settings) GetRaftPort() *types.Int32 { + return s.getInt32Ptr("keeper_server/raft_configuration/server/port") +} + +// MergeFrom merges into `dst` non-empty new-key-values from `from` in case no such `key` already in `src` +func (s *Settings) MergeFrom(from *Settings) *Settings { + if from.Len() == 0 { + return s + } + + from.Walk(func(key string, setting *Setting) { + s = s.Ensure().SetIfNotExists(key, setting.Clone()) + }) + + return s +} + +// MergeFromCB merges settings from src approved by filtering callback function +func (s *Settings) MergeFromCB(src *Settings, filter func(key string, setting *Setting) bool) *Settings { + if src.Len() == 0 { + return s + } + + src.Walk(func(key string, setting *Setting) { + if filter(key, setting) { + // Accept this setting + s = s.Ensure().Set(key, setting.Clone()) + } + }) + + return s +} + +// GetSection returns map of the specified settings section +func (s *Settings) GetSection(section SettingsSection, includeSettingWithNoSectionSpecified bool) (values map[string]string) { + if s == nil { + return nil + } + + s.WalkKeys(func(key string, setting *Setting) { + _section, err := GetSectionFromPath(key) + switch { + case (err == nil) && !_section.Equal(section): + // Section is specified in this key. + // And this is not the section we are looking for, skip to the next + return + case (err != nil) && (err != errorNoSectionSpecified): + // We have a complex error, skip to the next + return + case (err == errorNoSectionSpecified) && !includeSettingWithNoSectionSpecified: + // Section is not specified in this key. + // We are not ready to include setting with unspecified section, skip to the next + return + } + + // Looks like we are ready to include this setting into the result set + + filename, err := getFilenameFromPath(key) + if err != nil { + // We need to have filename specified + return + } + + if !setting.IsScalar() { + // We are ready to accept scalars only + return + } + + if values == nil { + // Lazy load + values = make(map[string]string) + } + + // Fetch file content + values[filename] = setting.ScalarString() + }) + + return values +} + +// Filter filters settings according to include and exclude lists +func (s *Settings) Filter( + includeSections []SettingsSection, + excludeSections []SettingsSection, + includeSettingWithNoSectionSpecified bool, +) (res *Settings) { + if s.Len() == 0 { + return res + } + + s.WalkKeys(func(key string, _ *Setting) { + section, err := GetSectionFromPath(key) + + if (err != nil) && (err != errorNoSectionSpecified) { + // We have a complex error, skip to the next + return + } + if (err == errorNoSectionSpecified) && !includeSettingWithNoSectionSpecified { + // We are not ready to include unspecified section, skip to the next + return + } + + // No include sections specified is treated as 'include by default' + include := section.In(includeSections) || (includeSections == nil) + exclude := section.In(excludeSections) + + if !include || exclude { + // This is not the section we are looking for, skip to the next + return + } + + // We'd like to get this setting + res = res.Ensure().SetKey(key, s.GetKey(key)) + }) + + return res +} + +// AsSortedSliceOfStrings return settings as sorted strings +func (s *Settings) AsSortedSliceOfStrings() []string { + if s == nil { + return nil + } + + // Sort keys + var keys []string + s.WalkKeys(func(key string, _ *Setting) { + keys = append(keys, key) + }) + sort.Strings(keys) + + var res []string + + // Walk over sorted keys + for _, key := range keys { + res = append(res, key) + res = append(res, s.GetKey(key).StringFull()) + } + + return res +} + +type SettingsNormalizerOptions struct { + Replacers []*util.Replacer +} + +// Normalize normalizes settings +func (s *Settings) Normalize(_opts ...*SettingsNormalizerOptions) *Settings { + s.normalizeKeys() + if len(_opts) > 0 { + opts := _opts[0] + macros := util.NewReplacerFrom(opts.Replacers...) + s.applyMacrosOnKeys(macros) + s.applyMacrosOnValues(macros) + } + return s +} + +// normalizeKeys normalizes keys in settings, treating them as paths +func (s *Settings) normalizeKeys() { + if s.Len() == 0 { + return + } + + var keysToNormalize []string + + // Find entries with keys to normalize + s.WalkKeys(func(key string, _ *Setting) { + if _, modified := normalizeKeyAsPath(key); modified { + // Normalization changed something. This path has to be normalized + keysToNormalize = append(keysToNormalize, key) + } + }) + + // Add entries with normalized keys + for _, unNormalizedKey := range keysToNormalize { + normalizedKey, _ := normalizeKeyAsPath(unNormalizedKey) + s.SetKey(normalizedKey, s.GetKey(unNormalizedKey)) + } + + // Delete entries with un-normalized keys + for _, unNormalizedKey := range keysToNormalize { + s.DeleteKey(unNormalizedKey) + } +} + +// applyMacrosOnKeys - applies macros on keys. Values are kept intact +func (s *Settings) applyMacrosOnKeys(macros *util.Replacer) { + if s.Len() == 0 { + return + } + + var keysToModify []string + + // Find keys which are to be modified + s.WalkKeys(func(key string, _ *Setting) { + if _, modified := macros.LineEx(key); modified { + // Applied macros will modify the key + keysToModify = append(keysToModify, key) + } + }) + + // Add entries with modified keys [modified key] => value + for _, originalKey := range keysToModify { + modifiedKey := macros.Line(originalKey) + s.SetKey(modifiedKey, s.GetKey(originalKey)) + } + + // Delete entries with before-modification keys + for _, beforeModificationKey := range keysToModify { + s.DeleteKey(beforeModificationKey) + } +} + +// applyMacrosOnValues - applies macros on values. Keys are kept intact +func (s *Settings) applyMacrosOnValues(macros *util.Replacer) { + s.Walk(func(name string, setting *Setting) { + setting.ApplyMacros(macros) + }) +} + +const xmlTagClickHouse = "clickhouse" + +// ClickHouseConfig produces ClickHouse config +func (s *Settings) ClickHouseConfig(_prefix ...string) string { + if s.Len() == 0 { + return "" + } + + prefix := "" + if len(_prefix) > 0 { + prefix = _prefix[0] + } + + b := &bytes.Buffer{} + // <clickhouse> + // XML code + // </clickhouse> + util.Iline(b, 0, "<"+xmlTagClickHouse+">") + xml.GenerateFromSettings(b, s, prefix) + util.Iline(b, 0, "</"+xmlTagClickHouse+">") + + return b.String() +} + +// normalizeKeyAsPath normalizes key which is treated as a path +// Normalized key looks like 'a/b/c' +// Used in in .spec.configuration.{users, profiles, quotas, settings, files} sections +func normalizeKeyAsPath(path string) (string, bool) { + // Find all multi-'/' values (like '//') + re := regexp.MustCompile("//+") + + // Squash all multi-'/' values (like '//') to single-'/' + normalized := re.ReplaceAllString(path, "/") + // Cut all leading and trailing '/', so the result would be 'a/b/c' + normalized = strings.Trim(normalized, "/") + + return normalized, normalized != path +} + +// getPrefixFromPath +func getPrefixFromPath(path string) (string, error) { + parts := strings.Split(path, "/") + if len(parts) < 2 { + // We need to have path to be at least 2 entries in order to have prefix + return "", errorNoPrefixSpecified + } + + // Extract the first component from the path + prefix := parts[0] + if prefix == "" { + return "", errorNoPrefixSpecified + } + + return prefix, nil +} + +// getSuffixFromPath +func getSuffixFromPath(path string) (string, error) { + parts := strings.Split(path, "/") + if len(parts) < 1 { + // We need to have path to be at least one entry - which will be the suffix + return "", errorNoSuffixSpecified + } + + // Extract the last component from the path + suffix := parts[len(parts)-1] + if suffix == "" { + // We need to have path to be at least one entry - which will be the suffix + return "", errorNoSuffixSpecified + } + + return suffix, nil +} + +// GetSectionFromPath +func GetSectionFromPath(path string) (SettingsSection, error) { + // String representation of the section + section, err := getPrefixFromPath(path) + if err != nil { + // We need to have path to be at least 'section/file.name' + return SectionEmpty, errorNoSectionSpecified + } + + // Check dir names to determine which section path points to + configDir := section + switch { + case strings.EqualFold(configDir, CommonConfigDirClickHouse): + return SectionCommon, nil + case strings.EqualFold(configDir, UsersConfigDirClickHouse): + return SectionUsers, nil + case strings.EqualFold(configDir, HostConfigDirClickHouse): + return SectionHost, nil + case strings.EqualFold(configDir, CommonConfigDirKeeper): + return SectionCommon, nil + case strings.EqualFold(configDir, UsersConfigDirKeeper): + return SectionUsers, nil + case strings.EqualFold(configDir, HostConfigDirKeeper): + return SectionHost, nil + } + + { + // TODO - either provide example or just remove this part + // Check explicitly specified sections. This is never(?) used + section := NewSettingsSectionFromString(section) + switch { + case SectionCommon.Equal(section): + return SectionCommon, nil + case SectionUsers.Equal(section): + return SectionUsers, nil + case SectionHost.Equal(section): + return SectionHost, nil + } + } + + return SectionEmpty, fmt.Errorf("unknown section specified %v", section) +} + +// getFilenameFromPath +func getFilenameFromPath(path string) (string, error) { + return getSuffixFromPath(path) +} + +// listModifiedSettingsPaths makes list of paths that were modified between two settings. +// Ex.: +// confid.d/setting1 +// confid.d/setting2 +func listModifiedSettingsPaths(a, b *Settings, path *messagediff.Path, value interface{}) (paths []string) { + if settings, ok := (value).(*Settings); ok { + // Provided `value` is of type api.Settings, which means that the whole + // settings such as 'files' or 'settings' is being either added or removed + if settings == nil { + // Completely removed settings such as 'files' or 'settings', so the value changed from Settings to nil + // List all entries from settings that are removed + for _, name := range a.Keys() { + paths = append(paths, name) + } + } else { + // Introduced new settings such as 'files' or 'settings', so the value changed from nil to Settings + // List all entries from settings that is added + for _, name := range b.Keys() { + paths = append(paths, name) + } + } + } else { + // Provided `value` is not of type api.Settings, thus expecting it to be a piece of settings. + // Modify (without full removal or addition) settings such as 'files' or 'settings', + // something is still left in the remaining part of settings in case of deletion or added in case of addition. + // Build string representation of path to updated element + var pathElements []string + for _, pathNode := range *path { + switch mk := pathNode.(type) { + case messagediff.MapKey: + switch pathElement := mk.Key.(type) { + case string: + pathElements = append(pathElements, pathElement) + } + } + } + paths = append(paths, strings.Join(pathElements, "/")) + } + + return paths +} + +// listPrefixedModifiedSettingsPaths makes list of paths that were modified between two settings. +// Each entry in the list is prefixed with the specified `pathPrefix` +// Ex.: `prefix` = file +// file/setting1 +// file/setting2 +func listPrefixedModifiedSettingsPaths(a, b *Settings, pathPrefix string, path *messagediff.Path, value interface{}) (paths []string) { + return util.Prefix(listModifiedSettingsPaths(a, b, path, value), pathPrefix+"/") +} + +// ListAffectedSettingsPathsFromDiff makes list of paths that were modified between two settings prefixed with the specified `prefix` +// Ex.: `prefix` = file +// file/setting1 +// file/setting2 +func ListAffectedSettingsPathsFromDiff(a, b *Settings, diff *messagediff.Diff, prefix string) (paths []string) { + for path, value := range diff.Added { + paths = append(paths, listPrefixedModifiedSettingsPaths(a, b, prefix, path, value)...) + } + for path, value := range diff.Removed { + paths = append(paths, listPrefixedModifiedSettingsPaths(a, b, prefix, path, value)...) + } + for path, value := range diff.Modified { + paths = append(paths, listPrefixedModifiedSettingsPaths(a, b, prefix, path, value)...) + } + return paths +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_name2key_converter_plain.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_name2key_converter_plain.go new file mode 100644 index 00000000..23eeb3a1 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_name2key_converter_plain.go @@ -0,0 +1,38 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// SettingsName2KeyConverterPlain implements plain name to key conversion +type SettingsName2KeyConverterPlain struct{} + +// NewSettingsName2KeyConverterPlain is a constructor +func NewSettingsName2KeyConverterPlain() SettingsName2KeyConverterPlain { + return SettingsName2KeyConverterPlain{} +} + +// Name2Key converts name to storage key. This is the opposite to Key2Name +func (s SettingsName2KeyConverterPlain) Name2Key(name string) string { + return name +} + +// Key2Name converts storage key to name. This is the opposite to Name2Key +func (s SettingsName2KeyConverterPlain) Key2Name(key string) string { + return key +} + +// DeepCopySettingsName2KeyConverter is required for code auto-generator +func (s SettingsName2KeyConverterPlain) DeepCopySettingsName2KeyConverter() SettingsName2KeyConverter { + return s +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_name2key_converter_prefix.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_name2key_converter_prefix.go new file mode 100644 index 00000000..fda30875 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_name2key_converter_prefix.go @@ -0,0 +1,56 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import "strings" + +// SettingsName2KeyConverterPrefix implements prefix name to key conversion +type SettingsName2KeyConverterPrefix struct { + prefix string + separator string +} + +// NewSettingsName2KeyConverterPrefix is a constructor +func NewSettingsName2KeyConverterPrefix(prefix string, separators ...string) SettingsName2KeyConverterPrefix { + // Default separator is "/" + separator := "/" + if len(separators) > 0 { + separator = separators[0] + } + return SettingsName2KeyConverterPrefix{ + prefix: prefix, + separator: separator, + } +} + +// Name2Key converts name to storage key. This is the opposite to Key2Name +func (s SettingsName2KeyConverterPrefix) Name2Key(name string) string { + return s.Prefix() + name +} + +// Key2Name converts storage key to name. This is the opposite to Name2Key +func (s SettingsName2KeyConverterPrefix) Key2Name(key string) string { + return strings.TrimPrefix(key, s.Prefix()) +} + +// Prefix gets full prefix +func (s SettingsName2KeyConverterPrefix) Prefix() string { + return s.prefix + s.separator +} + +// DeepCopySettingsName2KeyConverter is required for code auto-generator +func (s SettingsName2KeyConverterPrefix) DeepCopySettingsName2KeyConverter() SettingsName2KeyConverter { + return s +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_section.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_section.go new file mode 100644 index 00000000..5c472cec --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_section.go @@ -0,0 +1,63 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import "strings" + +// Configuration sections +// Each section renders into separate ConfigMap mapped into Pod as ClickHouse configuration file +var ( + SectionEmpty SettingsSection = "" + SectionCommon SettingsSection = "{common}" + SectionUsers SettingsSection = "{users}" + SectionHost SettingsSection = "{hosts}" +) + +// SettingsSection specifies settings section +type SettingsSection string + +// NewSettingsSectionFromString creates SettingsSection from a string +func NewSettingsSectionFromString(section string) SettingsSection { + switch { + case strings.EqualFold(section, SectionCommon.String()): + return SectionCommon + case strings.EqualFold(section, SectionUsers.String()): + return SectionUsers + case strings.EqualFold(section, SectionHost.String()): + return SectionHost + default: + return SectionEmpty + } +} + +// In checks whether needle is in haystack +func (s SettingsSection) In(haystack []SettingsSection) bool { + for _, item := range haystack { + if item == s { + return true + } + } + return false +} + +// String implements stringer +func (s SettingsSection) String() string { + return string(s) +} + +// Equal checks two SettingsSection for equality +func (s SettingsSection) Equal(another SettingsSection) bool { + return s.String() == another.String() +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_user.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_user.go new file mode 100644 index 00000000..27f3c3e7 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_settings_user.go @@ -0,0 +1,39 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// SettingsUser specifies settings of a user +type SettingsUser struct { + Settings + username string +} + +// NewSettingsUser creates new user settings +func NewSettingsUser(settings *Settings, username string) *SettingsUser { + s := &SettingsUser{ + Settings: *settings, + username: username, + } + s.Settings.SetConverter(NewSettingsName2KeyConverterPrefix(username)) + return s +} + +// Username gets username +func (s *SettingsUser) Username() string { + if s == nil { + return "" + } + return s.username +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_shard.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_shard.go new file mode 100644 index 00000000..e83f255d --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_shard.go @@ -0,0 +1,309 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +// ChiShard defines item of a shard section of .spec.configuration.clusters[n].shards +// TODO unify with ChiReplica based on HostsSet +type ChiShard struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Weight *int `json:"weight,omitempty" yaml:"weight,omitempty"` + InternalReplication *types.StringBool `json:"internalReplication,omitempty" yaml:"internalReplication,omitempty"` + Settings *Settings `json:"settings,omitempty" yaml:"settings,omitempty"` + Files *Settings `json:"files,omitempty" yaml:"files,omitempty"` + Templates *TemplatesList `json:"templates,omitempty" yaml:"templates,omitempty"` + ReplicasCount int `json:"replicasCount,omitempty" yaml:"replicasCount,omitempty"` + // TODO refactor into map[string]Host + Hosts []*Host `json:"replicas,omitempty" yaml:"replicas,omitempty"` + + Runtime ChiShardRuntime `json:"-" yaml:"-"` + + // DefinitionType is DEPRECATED - to be removed soon + DefinitionType string `json:"definitionType,omitempty" yaml:"definitionType,omitempty"` +} + +type ChiShardRuntime struct { + Address ChiShardAddress `json:"-" yaml:"-"` + CHI *ClickHouseInstallation `json:"-" yaml:"-" testdiff:"ignore"` +} + +func (r *ChiShardRuntime) GetAddress() IShardAddress { + return &r.Address +} + +func (r *ChiShardRuntime) GetCR() ICustomResource { + return r.CHI +} + +func (r *ChiShardRuntime) SetCR(cr ICustomResource) { + r.CHI = cr.(*ClickHouseInstallation) +} + +func (shard *ChiShard) GetName() string { + return shard.Name +} + +func (shard *ChiShard) GetInternalReplication() *types.StringBool { + return shard.InternalReplication +} + +// InheritSettingsFrom inherits settings from specified cluster +func (shard *ChiShard) InheritSettingsFrom(cluster *Cluster) { + shard.Settings = shard.Settings.MergeFrom(cluster.Settings) +} + +// InheritFilesFrom inherits files from specified cluster +func (shard *ChiShard) InheritFilesFrom(cluster *Cluster) { + shard.Files = shard.Files.MergeFrom(cluster.Files) +} + +// InheritTemplatesFrom inherits templates from specified cluster +func (shard *ChiShard) InheritTemplatesFrom(cluster *Cluster) { + shard.Templates = shard.Templates.MergeFrom(cluster.Templates, MergeTypeFillEmptyValues) + shard.Templates.HandleDeprecatedFields() +} + +// GetServiceTemplate gets service template +func (shard *ChiShard) GetServiceTemplate() (*ServiceTemplate, bool) { + if !shard.Templates.HasShardServiceTemplate() { + return nil, false + } + name := shard.Templates.GetShardServiceTemplate() + return shard.Runtime.CHI.GetServiceTemplate(name) +} + +// HasReplicasCount checks whether shard has replicas count specified +func (shard *ChiShard) HasReplicasCount() bool { + if shard == nil { + return false + } + + return shard.ReplicasCount > 0 +} + +// WalkHosts runs specified function on each host +func (shard *ChiShard) WalkHosts(f func(host *Host) error) []error { + if shard == nil { + return nil + } + + res := make([]error, 0) + + for replicaIndex := range shard.Hosts { + host := shard.Hosts[replicaIndex] + res = append(res, f(host)) + } + + return res +} + +// WalkHosts runs specified function on each host +func (shard *ChiShard) WalkHostsAbortOnError(f func(host *Host) error) error { + if shard == nil { + return nil + } + + for replicaIndex := range shard.Hosts { + host := shard.Hosts[replicaIndex] + if err := f(host); err != nil { + return err + } + } + + return nil +} + +// FindHost finds host by name or index. +// Expectations: name is expected to be a string, index is expected to be an int. +func (shard *ChiShard) FindHost(needle interface{}) (res *Host) { + shard.WalkHosts(func(host *Host) error { + switch v := needle.(type) { + case string: + if host.Runtime.Address.HostName == v { + res = host + } + case int: + if host.Runtime.Address.ShardScopeIndex == v { + res = host + } + } + return nil + }) + return +} + +// FirstHost finds first host in the shard +func (shard *ChiShard) FirstHost() *Host { + var result *Host + shard.WalkHosts(func(host *Host) error { + if result == nil { + result = host + } + return nil + }) + return result +} + +// HostsCount returns count of hosts in the shard +func (shard *ChiShard) HostsCount() int { + count := 0 + shard.WalkHosts(func(host *Host) error { + count++ + return nil + }) + return count +} + +// GetCHI gets Custom Resource of the shard +func (shard *ChiShard) GetCHI() *ClickHouseInstallation { + return shard.Runtime.CHI +} + +func (shard *ChiShard) GetAncestor() IShard { + return shard.GetCHI().GetAncestor().FindShard(shard.GetCluster().GetName(), shard.GetName()) +} + +// GetCluster gets cluster of the shard +func (shard *ChiShard) GetCluster() *Cluster { + return shard.Runtime.CHI.GetSpecT().Configuration.Clusters[shard.Runtime.Address.ClusterIndex] +} + +// HasWeight checks whether shard has applicable weight value specified +func (shard *ChiShard) HasWeight() bool { + if shard == nil { + return false + } + if shard.Weight == nil { + return false + } + return *shard.Weight >= 0 +} + +// GetWeight gets weight +func (shard *ChiShard) GetWeight() int { + if shard.HasWeight() { + return *shard.Weight + } + return 0 +} + +func (shard *ChiShard) GetRuntime() IShardRuntime { + if shard == nil { + return (*ChiShardRuntime)(nil) + } + return &shard.Runtime +} + +func (shard *ChiShard) HasSettings() bool { + return shard.GetSettings() != nil +} + +func (shard *ChiShard) GetSettings() *Settings { + if shard == nil { + return nil + } + return shard.Settings +} + +func (shard *ChiShard) HasFiles() bool { + return shard.GetFiles() != nil +} + +func (shard *ChiShard) GetFiles() *Settings { + if shard == nil { + return nil + } + return shard.Files +} + +func (shard *ChiShard) HasTemplates() bool { + return shard.GetTemplates() != nil +} + +func (shard *ChiShard) GetTemplates() *TemplatesList { + if shard == nil { + return nil + } + return shard.Templates +} + +func (shard *ChiShard) IsZero() bool { + return shard == nil +} + +func (shard *ChiShard) IsNonZero() bool { + return shard != nil +} + +// ChiShardAddress defines address of a shard within ClickHouseInstallation +type ChiShardAddress struct { + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + CHIName string `json:"chiName,omitempty" yaml:"chiName,omitempty"` + ClusterName string `json:"clusterName,omitempty" yaml:"clusterName,omitempty"` + ClusterIndex int `json:"clusterIndex,omitempty" yaml:"clusterIndex,omitempty"` + ShardName string `json:"shardName,omitempty" yaml:"shardName,omitempty"` + ShardIndex int `json:"shardIndex,omitempty" yaml:"shardIndex,omitempty"` +} + +func (a *ChiShardAddress) GetNamespace() string { + return a.Namespace +} + +func (a *ChiShardAddress) SetNamespace(namespace string) { + a.Namespace = namespace +} + +func (a *ChiShardAddress) GetCRName() string { + return a.CHIName +} + +func (a *ChiShardAddress) SetCRName(name string) { + a.CHIName = name +} + +func (a *ChiShardAddress) GetClusterName() string { + return a.ClusterName +} + +func (a *ChiShardAddress) SetClusterName(name string) { + a.ClusterName = name +} + +func (a *ChiShardAddress) GetClusterIndex() int { + return a.ClusterIndex +} + +func (a *ChiShardAddress) SetClusterIndex(index int) { + a.ClusterIndex = index +} + +func (a *ChiShardAddress) GetShardName() string { + return a.ShardName +} + +func (a *ChiShardAddress) SetShardName(name string) { + a.ShardName = name +} + +func (a *ChiShardAddress) GetShardIndex() int { + return a.ShardIndex +} + +func (a *ChiShardAddress) SetShardIndex(index int) { + a.ShardIndex = index +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_spec.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_spec.go new file mode 100644 index 00000000..1c546d04 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_spec.go @@ -0,0 +1,171 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +// ChiSpec defines spec section of ClickHouseInstallation resource +type ChiSpec struct { + TaskID *types.Id `json:"taskID,omitempty" yaml:"taskID,omitempty"` + Stop *types.StringBool `json:"stop,omitempty" yaml:"stop,omitempty"` + Restart *types.String `json:"restart,omitempty" yaml:"restart,omitempty"` + Troubleshoot *types.StringBool `json:"troubleshoot,omitempty" yaml:"troubleshoot,omitempty"` + Suspend *types.StringBool `json:"suspend,omitempty" yaml:"suspend,omitempty"` + NamespaceDomainPattern *types.String `json:"namespaceDomainPattern,omitempty" yaml:"namespaceDomainPattern,omitempty"` + Templating *ChiTemplating `json:"templating,omitempty" yaml:"templating,omitempty"` + Reconciling *ChiReconcile `json:"reconciling,omitempty" yaml:"reconciling,omitempty"` + Reconcile *ChiReconcile `json:"reconcile,omitempty" yaml:"reconcile,omitempty"` + Defaults *Defaults `json:"defaults,omitempty" yaml:"defaults,omitempty"` + Configuration *Configuration `json:"configuration,omitempty" yaml:"configuration,omitempty"` + Templates *Templates `json:"templates,omitempty" yaml:"templates,omitempty"` + UseTemplates []*TemplateRef `json:"useTemplates,omitempty" yaml:"useTemplates,omitempty"` +} + +// HasTaskID checks whether task id is specified +func (spec *ChiSpec) HasTaskID() bool { + if spec == nil { + return false + } + return spec.TaskID.HasValue() +} + +// GetTaskID gets task id as a string +func (spec *ChiSpec) GetTaskID() *types.Id { + if spec == nil { + return nil + } + return spec.TaskID +} + +func (spec *ChiSpec) GetStop() *types.StringBool { + if spec == nil { + return (*types.StringBool)(nil) + } + return spec.Stop +} + +func (spec *ChiSpec) GetRestart() *types.String { + if spec == nil { + return (*types.String)(nil) + } + return spec.Restart +} + +func (spec *ChiSpec) GetTroubleshoot() *types.StringBool { + if spec == nil { + return (*types.StringBool)(nil) + } + return spec.Troubleshoot +} + +func (spec *ChiSpec) GetNamespaceDomainPattern() *types.String { + if spec == nil { + return (*types.String)(nil) + } + return spec.NamespaceDomainPattern +} + +func (spec *ChiSpec) GetTemplating() *ChiTemplating { + if spec == nil { + return (*ChiTemplating)(nil) + } + return spec.Templating +} + +func (spec *ChiSpec) GetDefaults() *Defaults { + if spec == nil { + return (*Defaults)(nil) + } + return spec.Defaults +} + +func (spec *ChiSpec) GetConfiguration() IConfiguration { + if spec == nil { + return (*Configuration)(nil) + } + return spec.Configuration +} + +func (spec *ChiSpec) GetTemplates() *Templates { + if spec == nil { + return (*Templates)(nil) + } + return spec.Templates +} + +// MergeFrom merges from spec +func (spec *ChiSpec) MergeFrom(from *ChiSpec, _type MergeType) { + if from == nil { + return + } + + if spec == nil { + spec = &ChiSpec{} + } + + switch _type { + case MergeTypeFillEmptyValues: + if !spec.HasTaskID() { + spec.TaskID = spec.TaskID.MergeFrom(from.TaskID) + } + if !spec.Stop.HasValue() { + spec.Stop = spec.Stop.MergeFrom(from.Stop) + } + if !spec.Restart.HasValue() { + spec.Restart = spec.Restart.MergeFrom(from.Restart) + } + if !spec.Troubleshoot.HasValue() { + spec.Troubleshoot = spec.Troubleshoot.MergeFrom(from.Troubleshoot) + } + if !spec.NamespaceDomainPattern.HasValue() { + spec.NamespaceDomainPattern = spec.NamespaceDomainPattern.MergeFrom(from.NamespaceDomainPattern) + } + if !spec.Suspend.HasValue() { + spec.Suspend = spec.Suspend.MergeFrom(from.Suspend) + } + case MergeTypeOverrideByNonEmptyValues: + if from.HasTaskID() { + spec.TaskID = spec.TaskID.MergeFrom(from.TaskID) + } + if from.Stop.HasValue() { + // Override by non-empty values only + spec.Stop = from.Stop + } + if from.Restart.HasValue() { + // Override by non-empty values only + spec.Restart = spec.Restart.MergeFrom(from.Restart) + } + if from.Troubleshoot.HasValue() { + // Override by non-empty values only + spec.Troubleshoot = from.Troubleshoot + } + if from.NamespaceDomainPattern.HasValue() { + spec.NamespaceDomainPattern = spec.NamespaceDomainPattern.MergeFrom(from.NamespaceDomainPattern) + } + if from.Suspend.HasValue() { + spec.Suspend = spec.Suspend.MergeFrom(from.Suspend) + } + } + + spec.Templating = spec.Templating.MergeFrom(from.Templating, _type) + spec.Reconcile = spec.Reconcile.MergeFrom(from.Reconcile, _type) + spec.Defaults = spec.Defaults.MergeFrom(from.Defaults, _type) + spec.Configuration = spec.Configuration.MergeFrom(from.Configuration, _type) + spec.Templates = spec.Templates.MergeFrom(from.Templates, _type) + // TODO may be it would be wiser to make more intelligent merge + spec.UseTemplates = append(spec.UseTemplates, from.UseTemplates...) +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_status.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_status.go new file mode 100644 index 00000000..ecb20050 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_status.go @@ -0,0 +1,902 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "sort" + "sync" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/version" +) + +const ( + maxActions = 10 + maxErrors = 10 + maxTaskIDs = 10 +) + +// Possible CR statuses +const ( + StatusInProgress = "InProgress" + StatusCompleted = "Completed" + StatusAborted = "Aborted" + StatusTerminating = "Terminating" +) + +// Status defines status section of the custom resource. +// +// Note: application level reads and writes to Status fields should be done through synchronized getter/setter functions. +// While all of these fields need to be exported for JSON and YAML serialization/deserialization, we can at least audit +// that application logic sticks to the synchronized getter/setters by auditing whether all explicit Go field-level +// accesses are strictly within _this_ source file OR the generated deep copy source file. +type Status struct { + CHOpVersion string `json:"chop-version,omitempty" yaml:"chop-version,omitempty"` + CHOpCommit string `json:"chop-commit,omitempty" yaml:"chop-commit,omitempty"` + CHOpDate string `json:"chop-date,omitempty" yaml:"chop-date,omitempty"` + CHOpIP string `json:"chop-ip,omitempty" yaml:"chop-ip,omitempty"` + ClustersCount int `json:"clusters,omitempty" yaml:"clusters,omitempty"` + ShardsCount int `json:"shards,omitempty" yaml:"shards,omitempty"` + ReplicasCount int `json:"replicas,omitempty" yaml:"replicas,omitempty"` + HostsCount int `json:"hosts,omitempty" yaml:"hosts,omitempty"` + Status string `json:"status,omitempty" yaml:"status,omitempty"` + TaskID string `json:"taskID,omitempty" yaml:"taskID,omitempty"` + TaskIDsStarted []string `json:"taskIDsStarted,omitempty" yaml:"taskIDsStarted,omitempty"` + TaskIDsCompleted []string `json:"taskIDsCompleted,omitempty" yaml:"taskIDsCompleted,omitempty"` + Action string `json:"action,omitempty" yaml:"action,omitempty"` + Actions []string `json:"actions,omitempty" yaml:"actions,omitempty"` + Error string `json:"error,omitempty" yaml:"error,omitempty"` + Errors []string `json:"errors,omitempty" yaml:"errors,omitempty"` + HostsUpdatedCount int `json:"hostsUpdated,omitempty" yaml:"hostsUpdated,omitempty"` + HostsAddedCount int `json:"hostsAdded,omitempty" yaml:"hostsAdded,omitempty"` + HostsUnchangedCount int `json:"hostsUnchanged,omitempty" yaml:"hostsUnchanged,omitempty"` + HostsFailedCount int `json:"hostsFailed,omitempty" yaml:"hostsFailed,omitempty"` + HostsCompletedCount int `json:"hostsCompleted,omitempty" yaml:"hostsCompleted,omitempty"` + HostsDeletedCount int `json:"hostsDeleted,omitempty" yaml:"hostsDeleted,omitempty"` + HostsDeleteCount int `json:"hostsDelete,omitempty" yaml:"hostsDelete,omitempty"` + Pods []string `json:"pods,omitempty" yaml:"pods,omitempty"` + PodIPs []string `json:"pod-ips,omitempty" yaml:"pod-ips,omitempty"` + FQDNs []string `json:"fqdns,omitempty" yaml:"fqdns,omitempty"` + Endpoint string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"` + Endpoints []string `json:"endpoints,omitempty" yaml:"endpoints,omitempty"` + NormalizedCR *ClickHouseInstallation `json:"normalized,omitempty" yaml:"normalized,omitempty"` + NormalizedCRCompleted *ClickHouseInstallation `json:"normalizedCompleted,omitempty" yaml:"normalizedCompleted,omitempty"` + ActionPlan *ActionPlan `json:"actionPlan,omitempty" yaml:"actionPlan,omitempty"` + HostsWithTablesCreated []string `json:"hostsWithTablesCreated,omitempty" yaml:"hostsWithTablesCreated,omitempty"` + HostsWithReplicaCaughtUp []string `json:"hostsWithReplicaCaughtUp,omitempty" yaml:"hostsWithReplicaCaughtUp,omitempty"` + UsedTemplates []*TemplateRef `json:"usedTemplates,omitempty" yaml:"usedTemplates,omitempty"` + + mu sync.RWMutex `json:"-" yaml:"-"` +} + +// FillStatusParams is a struct used to fill status params +type FillStatusParams struct { + CHOpIP string + ClustersCount int + ShardsCount int + HostsCount int + TaskID string + HostsUpdatedCount int + HostsAddedCount int + HostsUnchangedCount int + HostsCompletedCount int + HostsDeleteCount int + HostsDeletedCount int + Pods []string + FQDNs []string + Endpoint string + Endpoints []string + NormalizedCR *ClickHouseInstallation +} + +// Fill is a synchronized setter for a fairly large number of fields. We take a struct type "params" argument to avoid +// confusion of similarly typed positional arguments, and to avoid defining a lot of separate synchronized setters +// for these fields that are typically all set together at once (during "fills"). +func (s *Status) Fill(params *FillStatusParams) { + doWithWriteLock(s, func(s *Status) { + // We always set these (build-hardcoded) version fields. + s.CHOpVersion = version.Version + s.CHOpCommit = version.GitSHA + s.CHOpDate = version.BuiltAt + + // Now, set fields from the provided input. + s.CHOpIP = params.CHOpIP + s.ClustersCount = params.ClustersCount + s.ShardsCount = params.ShardsCount + s.HostsCount = params.HostsCount + s.TaskID = params.TaskID + s.HostsUpdatedCount = params.HostsUpdatedCount + s.HostsAddedCount = params.HostsAddedCount + s.HostsUnchangedCount = params.HostsUnchangedCount + s.HostsCompletedCount = params.HostsCompletedCount + s.HostsDeleteCount = params.HostsDeleteCount + s.HostsDeletedCount = params.HostsDeletedCount + s.Pods = params.Pods + s.FQDNs = util.NormalizeFQDNs(params.FQDNs) + s.Endpoint = util.NormalizeFQDN(params.Endpoint) + s.Endpoints = util.NormalizeFQDNs(params.Endpoints) + s.NormalizedCR = params.NormalizedCR + }) +} + +// SetError sets status error +func (s *Status) SetError(err string) { + doWithWriteLock(s, func(s *Status) { + s.Error = err + }) +} + +// PushError sets and pushes error into status +func (s *Status) PushError(error string) { + doWithWriteLock(s, func(s *Status) { + s.Errors = append([]string{error}, s.Errors...) + if len(s.Errors) > maxErrors { + s.Errors = s.Errors[:maxErrors] + } + }) +} + +// SetAndPushError sets and pushes error into status +func (s *Status) SetAndPushError(err string) { + doWithWriteLock(s, func(s *Status) { + s.Error = err + s.Errors = append([]string{err}, s.Errors...) + if len(s.Errors) > maxErrors { + s.Errors = s.Errors[:maxErrors] + } + }) +} + +// PushHostReplicaCaughtUp pushes host to the list of hosts with replica caught-up +func (s *Status) PushHostReplicaCaughtUp(host string) { + host = util.NormalizeFQDN(host) + doWithWriteLock(s, func(s *Status) { + if util.InArray(host, s.HostsWithReplicaCaughtUp) { + return + } + s.HostsWithReplicaCaughtUp = append(s.HostsWithReplicaCaughtUp, host) + }) +} + +// PushHostTablesCreated pushes host to the list of hosts with created tables +func (s *Status) PushHostTablesCreated(host string) { + host = util.NormalizeFQDN(host) + doWithWriteLock(s, func(s *Status) { + if util.InArray(host, s.HostsWithTablesCreated) { + return + } + s.HostsWithTablesCreated = append(s.HostsWithTablesCreated, host) + }) +} + +// SyncHostTablesCreated syncs list of hosts with tables created with actual list of hosts +func (s *Status) SyncHostTablesCreated() { + doWithWriteLock(s, func(s *Status) { + if s.FQDNs == nil { + return + } + // Normalize both sides to handle status data written by older operator versions + // that may contain trailing dots. + s.HostsWithTablesCreated = util.IntersectStringArrays( + util.NormalizeFQDNs(s.HostsWithTablesCreated), + util.NormalizeFQDNs(s.FQDNs), + ) + }) +} + +// PushUsedTemplate pushes used templates to the list of used templates +func (s *Status) PushUsedTemplate(templateRefs ...*TemplateRef) { + if len(templateRefs) > 0 { + doWithWriteLock(s, func(s *Status) { + s.UsedTemplates = append(s.UsedTemplates, templateRefs...) + }) + } +} + +// GetUsedTemplatesCount gets used templates count +func (s *Status) GetUsedTemplatesCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return len(s.UsedTemplates) + }) +} + +// SetAction action setter +func (s *Status) SetAction(action string) { + doWithWriteLock(s, func(s *Status) { + s.Action = action + }) +} + +// PushAction pushes action into status +func (s *Status) PushAction(action string) { + doWithWriteLock(s, func(s *Status) { + s.Actions = append([]string{action}, s.Actions...) + trimActionsNoSync(s) + }) +} + +// HasNormalizedCRCompleted is a checker +func (s *Status) HasNormalizedCRCompleted() bool { + return s.GetNormalizedCRCompleted() != nil +} + +// HasNormalizedCR is a checker +func (s *Status) HasNormalizedCR() bool { + return s.GetNormalizedCR() != nil +} + +// SetPodIPs sets pod IPs +func (s *Status) SetPodIPs(podIPs []string) { + doWithWriteLock(s, func(s *Status) { + s.PodIPs = podIPs + }) +} + +// HostDeleted increments deleted hosts counter +func (s *Status) HostDeleted() { + doWithWriteLock(s, func(s *Status) { + s.HostsDeletedCount++ + }) +} + +// HostUpdated increments updated hosts counter +func (s *Status) HostUpdated() { + doWithWriteLock(s, func(s *Status) { + s.HostsUpdatedCount++ + }) +} + +// HostAdded increments added hosts counter +func (s *Status) HostAdded() { + doWithWriteLock(s, func(s *Status) { + s.HostsAddedCount++ + }) +} + +// HostUnchanged increments unchanged hosts counter +func (s *Status) HostUnchanged() { + doWithWriteLock(s, func(s *Status) { + s.HostsUnchangedCount++ + }) +} + +// HostFailed increments failed hosts counter +func (s *Status) HostFailed() { + doWithWriteLock(s, func(s *Status) { + s.HostsFailedCount++ + }) +} + +// HostCompleted increments completed hosts counter +func (s *Status) HostCompleted() { + doWithWriteLock(s, func(s *Status) { + s.HostsCompletedCount++ + }) +} + +// ReconcileStart marks reconcile start +func (s *Status) ReconcileStart(ap IActionPlan) { + doWithWriteLock(s, func(s *Status) { + if s == nil { + return + } + s.Status = StatusInProgress + s.HostsUpdatedCount = 0 + s.HostsAddedCount = 0 + s.HostsUnchangedCount = 0 + s.HostsCompletedCount = 0 + s.HostsDeletedCount = 0 + s.HostsDeleteCount = ap.GetRemovedHostsNum() + s.ActionPlan = ap.(*ActionPlan) + pushTaskIDStartedNoSync(s) + }) +} + +// ReconcileComplete marks reconcile completion +func (s *Status) ReconcileComplete() { + doWithWriteLock(s, func(s *Status) { + if s == nil { + return + } + s.Status = StatusCompleted + s.Action = "" + pushTaskIDCompletedNoSync(s) + }) +} + +// ReconcileAbort marks reconcile abortion +func (s *Status) ReconcileAbort() { + doWithWriteLock(s, func(s *Status) { + if s == nil { + return + } + s.Status = StatusAborted + s.Action = "" + pushTaskIDCompletedNoSync(s) + }) +} + +// DeleteStart marks deletion start +func (s *Status) DeleteStart() { + doWithWriteLock(s, func(s *Status) { + if s == nil { + return + } + s.Status = StatusTerminating + s.HostsUpdatedCount = 0 + s.HostsAddedCount = 0 + s.HostsUnchangedCount = 0 + s.HostsCompletedCount = 0 + s.HostsDeletedCount = 0 + s.HostsDeleteCount = 0 + pushTaskIDStartedNoSync(s) + }) +} + +// SetActionPlan sets action plan +func (s *Status) SetActionPlan(ap IActionPlan) { + doWithWriteLock(s, func(s *Status) { + s.ActionPlan = ap.(*ActionPlan) + }) +} + +func prepareOptions(opts types.CopyStatusOptions) types.CopyStatusOptions { + if opts.FieldGroupInheritable { + opts.Copy.TaskIDsStarted = true + opts.Copy.TaskIDsCompleted = true + opts.Copy.Actions = true + opts.Copy.Errors = true + opts.Copy.HostsWithTablesCreated = true + opts.Copy.UsedTemplates = true + } + + if opts.FieldGroupActions { + opts.Copy.Action = true + opts.Merge.Actions = true + opts.Copy.HostsWithTablesCreated = true + opts.Copy.UsedTemplates = true + } + + if opts.FieldGroupErrors { + opts.Copy.Error = true + opts.Merge.Errors = true + } + + if opts.FieldGroupMain { + opts.Copy.CHOpVersion = true + opts.Copy.CHOpCommit = true + opts.Copy.CHOpDate = true + opts.Copy.CHOpIP = true + opts.Copy.ClustersCount = true + opts.Copy.ShardsCount = true + opts.Copy.ReplicasCount = true + opts.Copy.HostsCount = true + opts.Copy.Status = true + opts.Copy.TaskID = true + opts.Copy.TaskIDsStarted = true + opts.Copy.TaskIDsCompleted = true + opts.Copy.Action = true + opts.Merge.Actions = true + opts.Copy.Error = true + opts.Copy.Errors = true + opts.Copy.HostsUpdatedCount = true + opts.Copy.HostsAddedCount = true + opts.Copy.HostsUnchangedCount = true + opts.Copy.HostsCompletedCount = true + opts.Copy.HostsDeletedCount = true + opts.Copy.HostsDeleteCount = true + opts.Copy.HostsWithTablesCreated = true + opts.Copy.Pods = true + opts.Copy.PodIPs = true + opts.Copy.FQDNs = true + opts.Copy.Endpoint = true + opts.Copy.NormalizedCR = true + opts.Copy.ActionPlan = true + opts.Copy.UsedTemplates = true + } + + if opts.FieldGroupNormalized { + opts.Copy.NormalizedCR = true + opts.Copy.ActionPlan = true + } + + if opts.FieldGroupWholeStatus { + opts.Copy.CHOpVersion = true + opts.Copy.CHOpCommit = true + opts.Copy.CHOpDate = true + opts.Copy.CHOpIP = true + opts.Copy.ClustersCount = true + opts.Copy.ShardsCount = true + opts.Copy.ReplicasCount = true + opts.Copy.HostsCount = true + opts.Copy.Status = true + opts.Copy.TaskID = true + opts.Copy.TaskIDsStarted = true + opts.Copy.TaskIDsCompleted = true + opts.Copy.Action = true + opts.Merge.Actions = true + opts.Copy.Error = true + opts.Copy.Errors = true + opts.Copy.HostsUpdatedCount = true + opts.Copy.HostsAddedCount = true + opts.Copy.HostsUnchangedCount = true + opts.Copy.HostsCompletedCount = true + opts.Copy.HostsDeletedCount = true + opts.Copy.HostsDeleteCount = true + opts.Copy.HostsWithTablesCreated = true + opts.Copy.Pods = true + opts.Copy.PodIPs = true + opts.Copy.FQDNs = true + opts.Copy.Endpoint = true + opts.Copy.NormalizedCR = true + opts.Copy.NormalizedCRCompleted = true + opts.Copy.ActionPlan = true + opts.Copy.UsedTemplates = true + } + + return opts +} + +// CopyFrom copies the state of a given Status f into the receiver Status of the call. +func (s *Status) CopyFrom(f *Status, opts types.CopyStatusOptions) { + doWithWriteLock(s, func(s *Status) { + doWithReadLock(f, func(from *Status) { + if s == nil || from == nil { + return + } + + opts = prepareOptions(opts) + + // Copy fields + if opts.Copy.CHOpVersion { + s.CHOpVersion = from.CHOpVersion + } + if opts.Copy.CHOpCommit { + s.CHOpCommit = from.CHOpCommit + } + if opts.Copy.CHOpDate { + s.CHOpDate = from.CHOpDate + } + if opts.Copy.CHOpIP { + s.CHOpIP = from.CHOpIP + } + if opts.Copy.ClustersCount { + s.ClustersCount = from.ClustersCount + } + if opts.Copy.ShardsCount { + s.ShardsCount = from.ShardsCount + } + if opts.Copy.ReplicasCount { + s.ReplicasCount = from.ReplicasCount + } + if opts.Copy.HostsCount { + s.HostsCount = from.HostsCount + } + if opts.Copy.Status { + s.Status = from.Status + } + if opts.Copy.TaskID { + s.TaskID = from.TaskID + } + if opts.Copy.TaskIDsStarted { + s.TaskIDsStarted = from.TaskIDsStarted + } + if opts.Copy.TaskIDsCompleted { + s.TaskIDsCompleted = from.TaskIDsCompleted + } + if opts.Copy.Action { + s.Action = from.Action + } + if opts.Merge.Actions { + mergeActionsNoSync(s, from) + } + if opts.Copy.Error { + s.Error = from.Error + } + if opts.Copy.Errors { + s.Errors = from.Errors + } + if opts.Merge.Errors { + s.Errors = util.MergeStringArrays(s.Errors, from.Errors) + sort.Sort(sort.Reverse(sort.StringSlice(s.Errors))) + } + if opts.Copy.HostsUpdatedCount { + s.HostsUpdatedCount = from.HostsUpdatedCount + } + if opts.Copy.HostsAddedCount { + s.HostsAddedCount = from.HostsAddedCount + } + if opts.Copy.HostsUnchangedCount { + s.HostsUnchangedCount = from.HostsUnchangedCount + } + if opts.Copy.HostsCompletedCount { + s.HostsCompletedCount = from.HostsCompletedCount + } + if opts.Copy.HostsDeletedCount { + s.HostsDeletedCount = from.HostsDeletedCount + } + if opts.Copy.HostsDeleteCount { + s.HostsDeleteCount = from.HostsDeleteCount + } + if opts.Copy.Pods { + s.Pods = from.Pods + } + if opts.Copy.PodIPs { + s.PodIPs = from.PodIPs + } + if opts.Copy.FQDNs { + s.FQDNs = from.FQDNs + } + if opts.Copy.Endpoint { + s.Endpoint = from.Endpoint + s.Endpoints = from.Endpoints + } + if opts.Copy.NormalizedCR { + s.NormalizedCR = from.NormalizedCR + } + if opts.Copy.NormalizedCRCompleted { + s.NormalizedCRCompleted = from.NormalizedCRCompleted + } + if opts.Copy.ActionPlan { + s.ActionPlan = from.ActionPlan + } + if opts.Copy.HostsWithTablesCreated { + s.HostsWithTablesCreated = nil + if len(from.HostsWithTablesCreated) > 0 { + s.HostsWithTablesCreated = append(s.HostsWithTablesCreated, from.HostsWithTablesCreated...) + } + s.HostsWithReplicaCaughtUp = nil + if len(from.HostsWithReplicaCaughtUp) > 0 { + s.HostsWithReplicaCaughtUp = append(s.HostsWithReplicaCaughtUp, from.HostsWithReplicaCaughtUp...) + } + } + if opts.Copy.UsedTemplates { + if len(from.UsedTemplates) > len(s.UsedTemplates) { + s.UsedTemplates = nil + s.UsedTemplates = append(s.UsedTemplates, from.UsedTemplates...) + } + } + }) + }) +} + +// ClearNormalizedCR clears normalized CR in status +func (s *Status) ClearNormalizedCR() { + doWithWriteLock(s, func(s *Status) { + s.NormalizedCR = nil + }) +} + +// SetNormalizedCompletedFromCurrentNormalized sets completed CR from current CR +func (s *Status) SetNormalizedCompletedFromCurrentNormalized() { + doWithWriteLock(s, func(s *Status) { + s.NormalizedCRCompleted = s.NormalizedCR + }) +} + +// GetCHOpVersion gets operator version +func (s *Status) GetCHOpVersion() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.CHOpVersion + }) +} + +// GetCHOpCommit gets operator build commit +func (s *Status) GetCHOpCommit() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.CHOpCommit + }) +} + +// GetCHOpDate gets operator build date +func (s *Status) GetCHOpDate() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.CHOpDate + }) +} + +// GetCHOpIP gets operator pod's IP +func (s *Status) GetCHOpIP() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.CHOpIP + }) +} + +// GetClustersCount gets clusters count +func (s *Status) GetClustersCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.ClustersCount + }) +} + +// GetShardsCount gets shards count +func (s *Status) GetShardsCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.ShardsCount + }) +} + +// GetReplicasCount gets replicas count +func (s *Status) GetReplicasCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.ReplicasCount + }) +} + +// GetHostsCount gets hosts count +func (s *Status) GetHostsCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsCount + }) +} + +// GetStatus gets status +func (s *Status) GetStatus() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.Status + }) +} + +// GetTaskID gets task ipd +func (s *Status) GetTaskID() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.TaskID + }) +} + +// GetTaskIDsStarted gets started task id +func (s *Status) GetTaskIDsStarted() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.TaskIDsStarted + }) +} + +// GetTaskIDsCompleted gets completed task id +func (s *Status) GetTaskIDsCompleted() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.TaskIDsCompleted + }) +} + +// GetAction gets last action +func (s *Status) GetAction() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.Action + }) +} + +// GetActions gets all actions +func (s *Status) GetActions() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.Actions + }) +} + +// GetError gets last error +func (s *Status) GetError() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.Error + }) +} + +// GetErrors gets all errors +func (s *Status) GetErrors() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.Errors + }) +} + +// GetHostsUpdatedCount gets updated hosts counter +func (s *Status) GetHostsUpdatedCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsUpdatedCount + }) +} + +// GetHostsAddedCount gets added hosts counter +func (s *Status) GetHostsAddedCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsAddedCount + }) +} + +// GetHostsUnchangedCount gets unchanged hosts counter +func (s *Status) GetHostsUnchangedCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsUnchangedCount + }) +} + +// GetHostsFailedCount gets failed hosts counter +func (s *Status) GetHostsFailedCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsFailedCount + }) +} + +// GetHostsCompletedCount gets completed hosts counter +func (s *Status) GetHostsCompletedCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsCompletedCount + }) +} + +// GetHostsDeletedCount gets deleted hosts counter +func (s *Status) GetHostsDeletedCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsDeletedCount + }) +} + +// GetHostsDeleteCount gets hosts to be deleted counter +func (s *Status) GetHostsDeleteCount() int { + return getIntWithReadLock(s, func(s *Status) int { + return s.HostsDeleteCount + }) +} + +// GetPods gets list of pods +func (s *Status) GetPods() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.Pods + }) +} + +// GetPodIPs gets list of pod ips +func (s *Status) GetPodIPs() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.PodIPs + }) +} + +// GetFQDNs gets list of all FQDNs of hosts +func (s *Status) GetFQDNs() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return s.FQDNs + }) +} + +// GetEndpoint gets API endpoint +func (s *Status) GetEndpoint() string { + return getStringWithReadLock(s, func(s *Status) string { + return s.Endpoint + }) +} + +// GetNormalizedCR gets target CR +func (s *Status) GetNormalizedCR() *ClickHouseInstallation { + return getCRWithReadLock(s, func(s *Status) *ClickHouseInstallation { + return s.NormalizedCR + }) +} + +// GetNormalizedCRCompleted gets completed CR +func (s *Status) GetNormalizedCRCompleted() *ClickHouseInstallation { + return getCRWithReadLock(s, func(s *Status) *ClickHouseInstallation { + return s.NormalizedCRCompleted + }) +} + +// GetHostsWithTablesCreated gets hosts with created tables +func (s *Status) GetHostsWithTablesCreated() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return util.NormalizeFQDNs(s.HostsWithTablesCreated) + }) +} + +// GetHostsWithReplicaCaughtUp gets hosts with replica caught-up +func (s *Status) GetHostsWithReplicaCaughtUp() []string { + return getStringArrWithReadLock(s, func(s *Status) []string { + return util.NormalizeFQDNs(s.HostsWithReplicaCaughtUp) + }) +} + +// Begin helpers + +func doWithWriteLock(s *Status, f func(*Status)) { + if s == nil { + return + } + + s.mu.Lock() + defer s.mu.Unlock() + f(s) +} + +func doWithReadLock(s *Status, f func(*Status)) { + if s == nil { + return + } + + s.mu.RLock() + defer s.mu.RUnlock() + f(s) +} + +func getIntWithReadLock(s *Status, f func(*Status) int) int { + var zeroVal int + if s == nil { + return zeroVal + } + + s.mu.RLock() + defer s.mu.RUnlock() + return f(s) +} + +func getStringWithReadLock(s *Status, f func(*Status) string) string { + var zeroVal string + if s == nil { + return zeroVal + } + + s.mu.RLock() + defer s.mu.RUnlock() + return f(s) +} + +func getCRWithReadLock(s *Status, f func(*Status) *ClickHouseInstallation) *ClickHouseInstallation { + var zeroVal *ClickHouseInstallation + if s == nil { + return zeroVal + } + + s.mu.RLock() + defer s.mu.RUnlock() + return f(s) +} + +func getStringArrWithReadLock(s *Status, f func(*Status) []string) []string { + emptyArr := make([]string, 0, 0) + if s == nil { + return emptyArr + } + + s.mu.RLock() + defer s.mu.RUnlock() + return f(s) +} + +// mergeActionsNoSync merges the actions of from into those of s (without synchronization, because synchronized +// functions call into this). +func mergeActionsNoSync(s *Status, from *Status) { + s.Actions = util.MergeStringArrays(s.Actions, from.Actions) + sort.Sort(sort.Reverse(sort.StringSlice(s.Actions))) + trimActionsNoSync(s) +} + +// trimActionsNoSync trims actions (without synchronization, because synchronized functions call into this). +func trimActionsNoSync(s *Status) { + if len(s.Actions) > maxActions { + s.Actions = s.Actions[:maxActions] + } +} + +// pushTaskIDStartedNoSync pushes task id into status +func pushTaskIDStartedNoSync(s *Status) { + s.TaskIDsStarted = append([]string{s.TaskID}, s.TaskIDsStarted...) + if len(s.TaskIDsStarted) > maxTaskIDs { + s.TaskIDsStarted = s.TaskIDsStarted[:maxTaskIDs] + } +} + +// pushTaskIDCompletedNoSync pushes task id into status w/o sync +func pushTaskIDCompletedNoSync(s *Status) { + s.TaskIDsCompleted = append([]string{s.TaskID}, s.TaskIDsCompleted...) + if len(s.TaskIDsCompleted) > maxTaskIDs { + s.TaskIDsCompleted = s.TaskIDsCompleted[:maxTaskIDs] + } +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_status_test.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_status_test.go new file mode 100644 index 00000000..45ed570b --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_status_test.go @@ -0,0 +1,253 @@ +// //go:build race +package v1 + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +var normalizedChiA = &ClickHouseInstallation{} +var fillParamsA = &FillStatusParams{ + CHOpIP: "1.2.3.4", + ClustersCount: 1, + ShardsCount: 2, + HostsCount: 3, + TaskID: "task-a", + HostsUpdatedCount: 4, + HostsAddedCount: 5, + HostsCompletedCount: 6, + HostsDeleteCount: 7, + HostsDeletedCount: 8, + Pods: []string{"pod-a-1", "pod-a-2"}, + FQDNs: []string{"fqdns-a-1", "fqdns-a-2"}, + Endpoint: "endpoint-a", + NormalizedCR: normalizedChiA, // fields not recursively checked, this is only used as a pointer +} + +var normalizedChiB = &ClickHouseInstallation{} +var fillParamsB = &FillStatusParams{ + CHOpIP: "5.6.7.8", + ClustersCount: 10, + ShardsCount: 20, + HostsCount: 30, + TaskID: "task-b", + HostsUpdatedCount: 40, + HostsAddedCount: 50, + HostsCompletedCount: 60, + HostsDeleteCount: 70, + HostsDeletedCount: 80, + Pods: []string{"pod-b-1", "pod-b-2"}, + FQDNs: []string{"fqdns-b-1", "fqdns-b-2"}, + Endpoint: "endpoint-b", + NormalizedCR: normalizedChiB, // fields not recursively checked, this is only used as a pointer +} + +var copyTestStatusFrom = &Status{ + CHOpVersion: "version-a", + CHOpCommit: "commit-a", + CHOpDate: "date-a", + CHOpIP: "ip-a", + ClustersCount: 1, + ShardsCount: 2, + ReplicasCount: 3, + HostsCount: 4, + Status: "status-a", + TaskID: "task-a", + TaskIDsStarted: []string{"task-a-started-1", "task-a-started-2"}, + TaskIDsCompleted: []string{"task-a-completed-1", "task-a-completed-2"}, + Action: "action-a", + Actions: []string{"action-a", "action-a-another"}, + Error: "error-a", + Errors: []string{"error-a", "error-a-another"}, + HostsUpdatedCount: 5, + HostsAddedCount: 6, + HostsUnchangedCount: 7, + HostsFailedCount: 8, + HostsCompletedCount: 9, + HostsDeletedCount: 10, + HostsDeleteCount: 11, + Pods: []string{"pod-a-1", "pod-a-2"}, + PodIPs: []string{"podIP-a-1", "podIP-a-2"}, + FQDNs: []string{"fqdns-a-1", "fqdns-a-2"}, + Endpoint: "endpt-a", + NormalizedCR: normalizedChiA, + NormalizedCRCompleted: normalizedChiA, + HostsWithTablesCreated: []string{"host-a-1", "host-a-2"}, +} + +// NB: These tests mostly exist to exercise synchronization and detect regressions related to them via the +// Golang race detector. See: https://go.dev/blog/race-detector +// In short, add -race to the go test flags when running this. +func Test_ChiStatus_BasicOperations_SingleStatus_ConcurrencyTest(t *testing.T) { + type testCase struct { + name string + goRoutineA func(s *Status) + goRoutineB func(s *Status) + postConditionsVerification func(tt *testing.T, s *Status) + } + for _, tc := range []testCase{ + { + name: "PushAction", + goRoutineA: func(s *Status) { + s.PushAction("foo") + }, + goRoutineB: func(s *Status) { + s.PushAction("bar") + }, + postConditionsVerification: func(tt *testing.T, s *Status) { + actual := s.GetActions() + require.Len(tt, actual, 2) + require.Contains(tt, actual, "foo") + require.Contains(tt, actual, "bar") + }, + }, + { + name: "PushError", + goRoutineA: func(s *Status) { + s.PushError("errA") + s.PushError("errB") + }, + goRoutineB: func(s *Status) { + s.PushError("errC") + }, + postConditionsVerification: func(tt *testing.T, s *Status) { + actual := s.GetErrors() + require.Len(t, actual, 3) + require.Contains(tt, actual, "errA") + require.Contains(tt, actual, "errB") + require.Contains(tt, actual, "errC") + }, + }, + { + name: "Fill", + goRoutineA: func(s *Status) { + s.Fill(fillParamsA) + }, + goRoutineB: func(s *Status) { + s.Fill(fillParamsB) + }, + postConditionsVerification: func(tt *testing.T, s *Status) { + // Fill performs hard updates (overwrites), not pushing/adding extra data. + // The winning goroutine should basically determine the resultant post-condition for every "filled" field. + var expectedParams *FillStatusParams + if s.CHOpIP == fillParamsA.CHOpIP { + expectedParams = fillParamsA + } else if s.CHOpIP == fillParamsB.CHOpIP { + expectedParams = fillParamsB + } else { + require.Fail(t, "Unexpected CHOpIP after FillStatus: %s", s.CHOpIP) + } + require.Equal(tt, expectedParams.CHOpIP, s.CHOpIP) + require.Equal(tt, expectedParams.ClustersCount, s.ClustersCount) + require.Equal(tt, expectedParams.ShardsCount, s.ShardsCount) + require.Equal(tt, expectedParams.HostsCount, s.HostsCount) + require.Equal(tt, expectedParams.TaskID, s.TaskID) + require.Equal(tt, expectedParams.HostsUpdatedCount, s.HostsUpdatedCount) + require.Equal(tt, expectedParams.HostsAddedCount, s.HostsAddedCount) + require.Equal(tt, expectedParams.HostsCompletedCount, s.HostsCompletedCount) + require.Equal(tt, expectedParams.HostsDeleteCount, s.HostsDeleteCount) + require.Equal(tt, expectedParams.HostsDeletedCount, s.HostsDeletedCount) + require.Equal(tt, expectedParams.Pods, s.Pods) + require.Equal(tt, expectedParams.FQDNs, s.FQDNs) + require.Equal(tt, expectedParams.Endpoint, s.Endpoint) + require.Equal(tt, expectedParams.NormalizedCR, s.NormalizedCR) + }, + }, + { + name: "CopyFrom", + goRoutineA: func(s *Status) { + s.PushAction("always-present-action") // CopyFrom preserves existing actions (does not clobber) + s.CopyFrom(copyTestStatusFrom, types.CopyStatusOptions{ + CopyStatusFieldGroup: types.CopyStatusFieldGroup{ + FieldGroupActions: true, + FieldGroupErrors: true, + FieldGroupMain: true, + FieldGroupWholeStatus: true, + FieldGroupInheritable: true, + }, + }) + }, + goRoutineB: func(s *Status) { + s.PushAction("additional-action") // this may or may not win the race, but the race will be sync + }, + postConditionsVerification: func(tt *testing.T, s *Status) { + if len(s.GetActions()) == len(copyTestStatusFrom.GetActions())+2 { + require.Equal(tt, copyTestStatusFrom.GetActions(), s.GetActions()) + require.Contains(tt, s.GetActions(), "always-present-action") + require.Contains(tt, s.GetActions(), "additional-action") + for _, action := range copyTestStatusFrom.GetActions() { + require.Contains(tt, s.GetActions(), action) + } + } else { + require.Equal(tt, len(copyTestStatusFrom.GetActions())+1, len(s.GetActions())) + require.Contains(tt, s.GetActions(), "additional-action") + for _, action := range copyTestStatusFrom.GetActions() { + require.Contains(tt, s.GetActions(), action) + } + } + require.Equal(tt, copyTestStatusFrom.GetAction(), s.GetAction()) + require.Equal(tt, copyTestStatusFrom.GetCHOpCommit(), s.GetCHOpCommit()) + require.Equal(tt, copyTestStatusFrom.GetCHOpDate(), s.GetCHOpDate()) + require.Equal(tt, copyTestStatusFrom.GetCHOpIP(), s.GetCHOpIP()) + require.Equal(tt, copyTestStatusFrom.GetCHOpVersion(), s.GetCHOpVersion()) + require.Equal(tt, copyTestStatusFrom.GetClustersCount(), s.GetClustersCount()) + require.Equal(tt, copyTestStatusFrom.GetEndpoint(), s.GetEndpoint()) + require.Equal(tt, copyTestStatusFrom.GetError(), s.GetError()) + require.Equal(tt, copyTestStatusFrom.GetErrors(), s.GetErrors()) + require.Equal(tt, copyTestStatusFrom.GetErrors(), s.GetErrors()) + require.Equal(tt, copyTestStatusFrom.GetFQDNs(), s.GetFQDNs()) + require.Equal(tt, copyTestStatusFrom.GetHostsAddedCount(), s.GetHostsAddedCount()) + require.Equal(tt, copyTestStatusFrom.GetHostsCompletedCount(), s.GetHostsCompletedCount()) + require.Equal(tt, copyTestStatusFrom.GetHostsCount(), s.GetHostsCount()) + require.Equal(tt, copyTestStatusFrom.GetHostsDeleteCount(), s.GetHostsDeleteCount()) + require.Equal(tt, copyTestStatusFrom.GetHostsDeletedCount(), s.GetHostsDeletedCount()) + require.Equal(tt, copyTestStatusFrom.GetHostsUpdatedCount(), s.GetHostsUpdatedCount()) + require.Equal(tt, copyTestStatusFrom.GetHostsWithTablesCreated(), s.GetHostsWithTablesCreated()) + require.Equal(tt, copyTestStatusFrom.GetHostsWithTablesCreated(), s.GetHostsWithTablesCreated()) + require.Equal(tt, copyTestStatusFrom.GetHostsWithTablesCreated(), s.GetHostsWithTablesCreated()) + require.Equal(tt, copyTestStatusFrom.GetNormalizedCR(), s.GetNormalizedCR()) + require.Equal(tt, copyTestStatusFrom.GetNormalizedCRCompleted(), s.GetNormalizedCRCompleted()) + require.Equal(tt, copyTestStatusFrom.GetPodIPs(), s.GetPodIPs()) + require.Equal(tt, copyTestStatusFrom.GetPods(), s.GetPods()) + require.Equal(tt, copyTestStatusFrom.GetReplicasCount(), s.GetReplicasCount()) + require.Equal(tt, copyTestStatusFrom.GetShardsCount(), s.GetShardsCount()) + require.Equal(tt, copyTestStatusFrom.GetStatus(), s.GetStatus()) + require.Equal(tt, copyTestStatusFrom.GetTaskID(), s.GetTaskID()) + require.Equal(tt, copyTestStatusFrom.GetTaskIDsCompleted(), s.GetTaskIDsCompleted()) + require.Equal(tt, copyTestStatusFrom.GetTaskIDsStarted(), s.GetTaskIDsStarted()) + }, + }, + } { + t.Run(tc.name, func(tt *testing.T) { + status := &Status{} + startWg := sync.WaitGroup{} + doneWg := sync.WaitGroup{} + startWg.Add(2) // We will make sure both goroutines begin execution, i.e., that they don't execute sequentially. + doneWg.Add(2) // We also need to synchronize the test over the completion of both. + + go func() { + startWg.Done() + startWg.Wait() // Block until the other goroutine has begun execution + tc.goRoutineA(status) + doneWg.Done() + }() + + go func() { + startWg.Done() + startWg.Wait() // Block until the other goroutine has begun execution + + tc.goRoutineB(status) + doneWg.Done() + }() + + doneWg.Wait() // Block until both goroutines have completed execution + + // Verify post-conditions + tc.postConditionsVerification(tt, status) + }) + } +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_storage_management.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_storage_management.go new file mode 100644 index 00000000..1c0761ac --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_storage_management.go @@ -0,0 +1,68 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// StorageManagement defines storage management config +type StorageManagement struct { + PVCProvisioner PVCProvisioner `json:"provisioner,omitempty" yaml:"provisioner,omitempty"` + PVCReclaimPolicy PVCReclaimPolicy `json:"reclaimPolicy,omitempty" yaml:"reclaimPolicy,omitempty"` +} + +// NewStorageManagement creates new StorageManagement +func NewStorageManagement() *StorageManagement { + return new(StorageManagement) +} + +// MergeFrom merges from specified object +func (storageManagement *StorageManagement) MergeFrom(from *StorageManagement, _type MergeType) *StorageManagement { + if from == nil { + return storageManagement + } + + if storageManagement == nil { + storageManagement = &StorageManagement{} + } + + switch _type { + case MergeTypeFillEmptyValues: + return storageManagement.mergeFromFillEmptyValues(from) + case MergeTypeOverrideByNonEmptyValues: + return storageManagement.mergeFromOverwriteByNonEmptyValues(from) + } + + return storageManagement +} + +// mergeFromFillEmptyValues fills empty values +func (storageManagement *StorageManagement) mergeFromFillEmptyValues(from *StorageManagement) *StorageManagement { + if storageManagement.PVCProvisioner.IsUnspecified() { + storageManagement.PVCProvisioner = from.PVCProvisioner + } + if storageManagement.PVCReclaimPolicy.IsUnspecified() { + storageManagement.PVCReclaimPolicy = from.PVCReclaimPolicy + } + return storageManagement +} + +// mergeFromOverwriteByNonEmptyValues overwrites by non-empty values +func (storageManagement *StorageManagement) mergeFromOverwriteByNonEmptyValues(from *StorageManagement) *StorageManagement { + if from.PVCProvisioner.IsSpecified() { + storageManagement.PVCProvisioner = from.PVCProvisioner + } + if from.PVCReclaimPolicy.IsSpecified() { + storageManagement.PVCReclaimPolicy = from.PVCReclaimPolicy + } + return storageManagement +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_target_selector.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_target_selector.go new file mode 100644 index 00000000..0b82faba --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_target_selector.go @@ -0,0 +1,48 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// TargetSelector specifies target selector based on labels +type TargetSelector map[string]string + +// Matches checks whether TargetSelector matches provided set of labels +func (s TargetSelector) Matches(labels map[string]string) bool { + if s == nil { + // Empty selector matches all labels + return true + } + + // Walk over selector keys + for key, selectorValue := range s { + if labelValue, ok := labels[key]; !ok { + // Labels have no key specified in selector. + // Selector does not match the labels + return false + } else if selectorValue != labelValue { + // Labels have the key specified in selector, but selector value is not the same as labels value + // Selector does not match the labels + return false + } else { + // Selector value and label value are equal + // So far label matches selector + // Continue iteration to next value + } + } + + // All keys are in place with the same values + // Selector matches the labels + + return true +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_template_indexes.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_template_indexes.go new file mode 100644 index 00000000..cf59e18d --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_template_indexes.go @@ -0,0 +1,231 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// HostTemplatesIndex describes index of host templates +type HostTemplatesIndex struct { + // templates maps 'name of the template' -> 'template itself' + templates map[string]*HostTemplate `testdiff:"ignore"` +} + +// NewHostTemplatesIndex creates new HostTemplatesIndex object +func NewHostTemplatesIndex() *HostTemplatesIndex { + return &HostTemplatesIndex{ + templates: make(map[string]*HostTemplate), + } +} + +// Has checks whether index has entity `name` +func (i *HostTemplatesIndex) Has(name string) bool { + if i == nil { + return false + } + if i.templates == nil { + return false + } + _, ok := i.templates[name] + return ok +} + +// Get returns entity `name` from the index +func (i *HostTemplatesIndex) Get(name string) *HostTemplate { + if !i.Has(name) { + return nil + } + return i.templates[name] +} + +// Set sets named template into index +func (i *HostTemplatesIndex) Set(name string, entry *HostTemplate) { + if i == nil { + return + } + if i.templates == nil { + return + } + i.templates[name] = entry +} + +// Walk calls specified function over each entry in the index +func (i *HostTemplatesIndex) Walk(f func(template *HostTemplate)) { + if i == nil { + return + } + for _, entry := range i.templates { + f(entry) + } +} + +// PodTemplatesIndex describes index of pod templates +type PodTemplatesIndex struct { + // templates maps 'name of the template' -> 'template itself' + templates map[string]*PodTemplate `testdiff:"ignore"` +} + +// NewPodTemplatesIndex creates new PodTemplatesIndex object +func NewPodTemplatesIndex() *PodTemplatesIndex { + return &PodTemplatesIndex{ + templates: make(map[string]*PodTemplate), + } +} + +// Has checks whether index has entity `name` +func (i *PodTemplatesIndex) Has(name string) bool { + if i == nil { + return false + } + if i.templates == nil { + return false + } + _, ok := i.templates[name] + return ok +} + +// Get returns entity `name` from the index +func (i *PodTemplatesIndex) Get(name string) *PodTemplate { + if !i.Has(name) { + return nil + } + return i.templates[name] +} + +// Set sets named template into index +func (i *PodTemplatesIndex) Set(name string, entry *PodTemplate) { + if i == nil { + return + } + if i.templates == nil { + return + } + i.templates[name] = entry +} + +// Walk calls specified function over each entry in the index +func (i *PodTemplatesIndex) Walk(f func(template *PodTemplate)) { + if i == nil { + return + } + for _, entry := range i.templates { + f(entry) + } +} + +// VolumeClaimTemplatesIndex describes index of volume claim templates +type VolumeClaimTemplatesIndex struct { + // templates maps 'name of the template' -> 'template itself' + templates map[string]*VolumeClaimTemplate `testdiff:"ignore"` +} + +// NewVolumeClaimTemplatesIndex creates new VolumeClaimTemplatesIndex object +func NewVolumeClaimTemplatesIndex() *VolumeClaimTemplatesIndex { + return &VolumeClaimTemplatesIndex{ + templates: make(map[string]*VolumeClaimTemplate), + } +} + +// Has checks whether index has entity `name` +func (i *VolumeClaimTemplatesIndex) Has(name string) bool { + if i == nil { + return false + } + if i.templates == nil { + return false + } + _, ok := i.templates[name] + return ok +} + +// Get returns entity `name` from the index +func (i *VolumeClaimTemplatesIndex) Get(name string) *VolumeClaimTemplate { + if !i.Has(name) { + return nil + } + return i.templates[name] +} + +// Set sets named template into index +func (i *VolumeClaimTemplatesIndex) Set(name string, entry *VolumeClaimTemplate) { + if i == nil { + return + } + if i.templates == nil { + return + } + i.templates[name] = entry +} + +// Walk calls specified function over each entry in the index +func (i *VolumeClaimTemplatesIndex) Walk(f func(template *VolumeClaimTemplate)) { + if i == nil { + return + } + for _, entry := range i.templates { + f(entry) + } +} + +// ServiceTemplatesIndex describes index of service templates +type ServiceTemplatesIndex struct { + // templates maps 'name of the template' -> 'template itself' + templates map[string]*ServiceTemplate `testdiff:"ignore"` +} + +// NewServiceTemplatesIndex creates new ServiceTemplatesIndex object +func NewServiceTemplatesIndex() *ServiceTemplatesIndex { + return &ServiceTemplatesIndex{ + templates: make(map[string]*ServiceTemplate), + } +} + +// Has checks whether index has entity `name` +func (i *ServiceTemplatesIndex) Has(name string) bool { + if i == nil { + return false + } + if i.templates == nil { + return false + } + _, ok := i.templates[name] + return ok +} + +// Get returns entity `name` from the index +func (i *ServiceTemplatesIndex) Get(name string) *ServiceTemplate { + if !i.Has(name) { + return nil + } + return i.templates[name] +} + +// Set sets named template into index +func (i *ServiceTemplatesIndex) Set(name string, entry *ServiceTemplate) { + if i == nil { + return + } + if i.templates == nil { + return + } + i.templates[name] = entry +} + +// Walk calls specified function over each entry in the index +func (i *ServiceTemplatesIndex) Walk(f func(template *ServiceTemplate)) { + if i == nil { + return + } + for _, entry := range i.templates { + f(entry) + } +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_template_ref.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_template_ref.go new file mode 100644 index 00000000..eb242e4f --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_template_ref.go @@ -0,0 +1,22 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// TemplateRef defines UseTemplate section of ClickHouseInstallation resource +type TemplateRef struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + UseType string `json:"useType,omitempty" yaml:"useType,omitempty"` +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_templates.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_templates.go new file mode 100644 index 00000000..f528b369 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_templates.go @@ -0,0 +1,433 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "github.com/imdario/mergo" + + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Templates defines templates section of .spec +type Templates struct { + // Templates + HostTemplates []HostTemplate `json:"hostTemplates,omitempty" yaml:"hostTemplates,omitempty"` + PodTemplates []PodTemplate `json:"podTemplates,omitempty" yaml:"podTemplates,omitempty"` + VolumeClaimTemplates []VolumeClaimTemplate `json:"volumeClaimTemplates,omitempty" yaml:"volumeClaimTemplates,omitempty"` + ServiceTemplates []ServiceTemplate `json:"serviceTemplates,omitempty" yaml:"serviceTemplates,omitempty"` + + // Index maps template name to template itself + HostTemplatesIndex *HostTemplatesIndex `json:",omitempty" yaml:",omitempty" testdiff:"ignore"` + PodTemplatesIndex *PodTemplatesIndex `json:",omitempty" yaml:",omitempty" testdiff:"ignore"` + VolumeClaimTemplatesIndex *VolumeClaimTemplatesIndex `json:",omitempty" yaml:",omitempty" testdiff:"ignore"` + ServiceTemplatesIndex *ServiceTemplatesIndex `json:",omitempty" yaml:",omitempty" testdiff:"ignore"` +} + +// HostTemplate defines full Host Template +type HostTemplate struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + PortDistribution []PortDistribution `json:"portDistribution,omitempty" yaml:"portDistribution,omitempty"` + Spec Host `json:"spec,omitempty" yaml:"spec,omitempty"` +} + +// PortDistribution defines port distribution +type PortDistribution struct { + Type string `json:"type,omitempty" yaml:"type,omitempty"` +} + +// PodTemplate defines full Pod Template, directly used by StatefulSet +type PodTemplate struct { + Name string `json:"name" yaml:"name"` + GenerateName string `json:"generateName,omitempty" yaml:"generateName,omitempty"` + Zone PodTemplateZone `json:"zone,omitempty" yaml:"zone,omitempty"` + PodDistribution []PodDistribution `json:"podDistribution,omitempty" yaml:"podDistribution,omitempty"` + ObjectMeta meta.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"` + Spec core.PodSpec `json:"spec,omitempty" yaml:"spec,omitempty"` +} + +func (s *PodTemplate) HasGenerateName() bool { + if s == nil { + return false + } + return s.GenerateName != "" +} + +func (s *PodTemplate) GetGenerateName() string { + if s == nil { + return "" + } + return s.GenerateName +} + +func (s *PodTemplate) MergeFrom(from *PodTemplate) *PodTemplate { + if from == nil { + return s + } + + if s == nil { + s = new(PodTemplate) + } + + //toSpec := &toTemplate.Spec + //fromSpec := &fromTemplate.Spec + //_ = mergo.Merge(toSpec, *fromSpec, mergo.WithGrowSlice, mergo.WithOverride, mergo.WithOverrideEmptySlice) + + // Merge `to` template with `from` template + + backup := s.DeepCopy() + + _ = mergo.Merge(s, *from, mergo.WithSliceDeepMerge) + + // Deal with env explicitly + for i := range backup.Spec.Containers { + if i >= len(from.Spec.Containers) { + break + } + b := &backup.Spec.Containers[i] + s := &s.Spec.Containers[i] + f := &from.Spec.Containers[i] + s.Env = nil + s.Env = append(s.Env, b.Env...) + s.Env = append(s.Env, f.Env...) + } + + return s +} + +// PodTemplateZone defines pod template zone +type PodTemplateZone struct { + Key string `json:"key,omitempty" yaml:"key,omitempty"` + Values []string `json:"values,omitempty" yaml:"values,omitempty"` +} + +// PodDistribution defines pod distribution +type PodDistribution struct { + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Scope string `json:"scope,omitempty" yaml:"scope,omitempty"` + Number int `json:"number,omitempty" yaml:"number,omitempty"` + TopologyKey string `json:"topologyKey,omitempty" yaml:"topologyKey,omitempty"` +} + +// ServiceTemplate defines CHI service template +type ServiceTemplate struct { + Name string `json:"name" yaml:"name"` + GenerateName string `json:"generateName,omitempty" yaml:"generateName,omitempty"` + ObjectMeta meta.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"` + Spec core.ServiceSpec `json:"spec,omitempty" yaml:"spec,omitempty"` +} + +func (s *ServiceTemplate) HasGenerateName() bool { + if s == nil { + return false + } + return s.GenerateName != "" +} + +func (s *ServiceTemplate) GetGenerateName() string { + if s == nil { + return "" + } + return s.GenerateName +} + +// NewTemplates creates new Templates object +func NewTemplates() *Templates { + return new(Templates) +} + +func (templates *Templates) GetHostTemplates() []HostTemplate { + if templates == nil { + return nil + } + return templates.HostTemplates +} + +func (templates *Templates) GetPodTemplates() []PodTemplate { + if templates == nil { + return nil + } + return templates.PodTemplates +} + +func (templates *Templates) GetVolumeClaimTemplates() []VolumeClaimTemplate { + if templates == nil { + return nil + } + return templates.VolumeClaimTemplates +} + +func (templates *Templates) GetServiceTemplates() []ServiceTemplate { + if templates == nil { + return nil + } + return templates.ServiceTemplates +} + +// Len returns accumulated len of all templates +func (templates *Templates) Len() int { + if templates == nil { + return 0 + } + + return 0 + + len(templates.HostTemplates) + + len(templates.PodTemplates) + + len(templates.VolumeClaimTemplates) + + len(templates.ServiceTemplates) +} + +// MergeFrom merges from specified object +func (templates *Templates) MergeFrom(_from any, _type MergeType) *Templates { + // Typed from + var from *Templates + + // Ensure type + switch typed := _from.(type) { + case *Templates: + from = typed + default: + return templates + } + + // Sanity check + + if from.Len() == 0 { + return templates + } + + if templates == nil { + templates = NewTemplates() + } + + // Merge sections + + templates.mergeHostTemplates(from) + templates.mergePodTemplates(from) + templates.mergeVolumeClaimTemplates(from) + templates.mergeServiceTemplates(from) + + return templates +} + +// mergeHostTemplates merges host templates section +func (templates *Templates) mergeHostTemplates(from *Templates) { + if len(from.HostTemplates) == 0 { + return + } + + // We have templates to merge from + // Loop over all 'from' templates and either copy it in case no such template in receiver or merge it + for fromIndex := range from.HostTemplates { + fromTemplate := &from.HostTemplates[fromIndex] + + // Try to find entry with the same name among local templates in receiver + sameNameFound := false + for toIndex := range templates.HostTemplates { + toTemplate := &templates.HostTemplates[toIndex] + if toTemplate.Name == fromTemplate.Name { + // Receiver already have such a template + sameNameFound = true + // Merge `to` template with `from` template + _ = mergo.Merge(toTemplate, *fromTemplate, mergo.WithSliceDeepMerge) + // Receiver `to` template is processed + break + } + } + + if !sameNameFound { + // Receiver does not have template with such a name + // Append template from `from` + templates.HostTemplates = append(templates.HostTemplates, *fromTemplate.DeepCopy()) + } + } +} + +// mergePodTemplates merges pod templates section +func (templates *Templates) mergePodTemplates(from *Templates) { + if len(from.PodTemplates) == 0 { + return + } + + // We have templates to merge from + // Loop over all 'from' templates and either copy it in case no such template in receiver or merge it + for fromIndex := range from.PodTemplates { + fromTemplate := &from.PodTemplates[fromIndex] + + // Try to find entry with the same name among local templates in receiver + sameNameFound := false + for toIndex := range templates.PodTemplates { + toTemplate := &templates.PodTemplates[toIndex] + if toTemplate.Name == fromTemplate.Name { + // Receiver already have such a template + sameNameFound = true + toTemplate.MergeFrom(fromTemplate) + break + } + } + + if !sameNameFound { + // Receiver does not have template with such a name + // Append template from `from` + templates.PodTemplates = append(templates.PodTemplates, *fromTemplate.DeepCopy()) + } + } +} + +// mergeVolumeClaimTemplates merges volume claim templates section +func (templates *Templates) mergeVolumeClaimTemplates(from *Templates) { + if len(from.VolumeClaimTemplates) == 0 { + return + } + + // We have templates to merge from + // Loop over all 'from' templates and either copy it in case no such template in receiver or merge it + for fromIndex := range from.VolumeClaimTemplates { + fromTemplate := &from.VolumeClaimTemplates[fromIndex] + + // Try to find entry with the same name among local templates in receiver + sameNameFound := false + for toIndex := range templates.VolumeClaimTemplates { + toTemplate := &templates.VolumeClaimTemplates[toIndex] + if toTemplate.Name == fromTemplate.Name { + // Receiver already have such a template + sameNameFound = true + // Merge `to` template with `from` template + _ = mergo.Merge(toTemplate, *fromTemplate, mergo.WithSliceDeepMerge) + // Receiver `to` template is processed + break + } + } + + if !sameNameFound { + // Receiver does not have template with such a name + // Append template from `from` + templates.VolumeClaimTemplates = append(templates.VolumeClaimTemplates, *fromTemplate.DeepCopy()) + } + } +} + +// mergeServiceTemplates merges service templates section +func (templates *Templates) mergeServiceTemplates(from *Templates) { + if len(from.ServiceTemplates) == 0 { + return + } + + // We have templates to merge from + // Loop over all 'from' templates and either copy it in case no such template in receiver or merge it + for fromIndex := range from.ServiceTemplates { + fromTemplate := &from.ServiceTemplates[fromIndex] + + // Try to find entry with the same name among local templates in receiver + sameNameFound := false + for toIndex := range templates.ServiceTemplates { + toTemplate := &templates.ServiceTemplates[toIndex] + if toTemplate.Name == fromTemplate.Name { + // Receiver already have such a template + sameNameFound = true + // Merge `to` template with `from` template + _ = mergo.Merge(toTemplate, *fromTemplate, mergo.WithSliceDeepCopy) + // Receiver `to` template is processed + break + } + } + + if !sameNameFound { + // Receiver does not have template with such a name + // Append template from `from` + templates.ServiceTemplates = append(templates.ServiceTemplates, *fromTemplate.DeepCopy()) + } + } +} + +// GetHostTemplatesIndex returns index of host templates +func (templates *Templates) GetHostTemplatesIndex() *HostTemplatesIndex { + if templates == nil { + return nil + } + return templates.HostTemplatesIndex +} + +// EnsureHostTemplatesIndex ensures index exists +func (templates *Templates) EnsureHostTemplatesIndex() *HostTemplatesIndex { + if templates == nil { + return nil + } + if templates.HostTemplatesIndex != nil { + return templates.HostTemplatesIndex + } + templates.HostTemplatesIndex = NewHostTemplatesIndex() + return templates.HostTemplatesIndex +} + +// GetPodTemplatesIndex returns index of pod templates +func (templates *Templates) GetPodTemplatesIndex() *PodTemplatesIndex { + if templates == nil { + return nil + } + return templates.PodTemplatesIndex +} + +// EnsurePodTemplatesIndex ensures index exists +func (templates *Templates) EnsurePodTemplatesIndex() *PodTemplatesIndex { + if templates == nil { + return nil + } + if templates.PodTemplatesIndex != nil { + return templates.PodTemplatesIndex + } + templates.PodTemplatesIndex = NewPodTemplatesIndex() + return templates.PodTemplatesIndex +} + +// GetVolumeClaimTemplatesIndex returns index of VolumeClaim templates +func (templates *Templates) GetVolumeClaimTemplatesIndex() *VolumeClaimTemplatesIndex { + if templates == nil { + return nil + } + return templates.VolumeClaimTemplatesIndex +} + +// EnsureVolumeClaimTemplatesIndex ensures index exists +func (templates *Templates) EnsureVolumeClaimTemplatesIndex() *VolumeClaimTemplatesIndex { + if templates == nil { + return nil + } + if templates.VolumeClaimTemplatesIndex != nil { + return templates.VolumeClaimTemplatesIndex + } + templates.VolumeClaimTemplatesIndex = NewVolumeClaimTemplatesIndex() + return templates.VolumeClaimTemplatesIndex +} + +// GetServiceTemplatesIndex returns index of Service templates +func (templates *Templates) GetServiceTemplatesIndex() *ServiceTemplatesIndex { + if templates == nil { + return nil + } + return templates.ServiceTemplatesIndex +} + +// EnsureServiceTemplatesIndex ensures index exists +func (templates *Templates) EnsureServiceTemplatesIndex() *ServiceTemplatesIndex { + if templates == nil { + return nil + } + if templates.ServiceTemplatesIndex != nil { + return templates.ServiceTemplatesIndex + } + templates.ServiceTemplatesIndex = NewServiceTemplatesIndex() + return templates.ServiceTemplatesIndex +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_templates_list.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_templates_list.go new file mode 100644 index 00000000..0a1acca2 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_templates_list.go @@ -0,0 +1,305 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" + +// TemplatesList defines references to .spec.templates to be used +type TemplatesList struct { + HostTemplate string `json:"hostTemplate,omitempty" yaml:"hostTemplate,omitempty"` + PodTemplate string `json:"podTemplate,omitempty" yaml:"podTemplate,omitempty"` + DataVolumeClaimTemplate string `json:"dataVolumeClaimTemplate,omitempty" yaml:"dataVolumeClaimTemplate,omitempty"` + LogVolumeClaimTemplate string `json:"logVolumeClaimTemplate,omitempty" yaml:"logVolumeClaimTemplate,omitempty"` + ServiceTemplate string `json:"serviceTemplate,omitempty" yaml:"serviceTemplate,omitempty"` + ServiceTemplates []string `json:"serviceTemplates,omitempty" yaml:"serviceTemplates,omitempty"` + ClusterServiceTemplate string `json:"clusterServiceTemplate,omitempty" yaml:"clusterServiceTemplate,omitempty"` + ShardServiceTemplate string `json:"shardServiceTemplate,omitempty" yaml:"shardServiceTemplate,omitempty"` + ReplicaServiceTemplate string `json:"replicaServiceTemplate,omitempty" yaml:"replicaServiceTemplate,omitempty"` + + // VolumeClaimTemplate is deprecated in favor of DataVolumeClaimTemplate and LogVolumeClaimTemplate + // !!! DEPRECATED !!! + VolumeClaimTemplate string `json:"volumeClaimTemplate,omitempty" yaml:"volumeClaimTemplate,omitempty"` +} + +// NewTemplatesList creates new TemplatesList object +func NewTemplatesList() *TemplatesList { + return new(TemplatesList) +} + +// HasHostTemplate checks whether host template is specified +func (tl *TemplatesList) HasHostTemplate() bool { + if tl == nil { + return false + } + return len(tl.HostTemplate) > 0 +} + +// GetHostTemplate gets host template +func (tl *TemplatesList) GetHostTemplate() string { + if tl == nil { + return "" + } + return tl.HostTemplate +} + +// HasPodTemplate checks whether pod template is specified +func (tl *TemplatesList) HasPodTemplate() bool { + if tl == nil { + return false + } + return len(tl.PodTemplate) > 0 +} + +// GetPodTemplate gets pod template +func (tl *TemplatesList) GetPodTemplate() string { + if tl == nil { + return "" + } + return tl.PodTemplate +} + +// HasDataVolumeClaimTemplate checks whether data volume claim template is specified +func (tl *TemplatesList) HasDataVolumeClaimTemplate() bool { + if tl == nil { + return false + } + return len(tl.DataVolumeClaimTemplate) > 0 +} + +// GetDataVolumeClaimTemplate gets data volume claim template +func (tl *TemplatesList) GetDataVolumeClaimTemplate() string { + if tl == nil { + return "" + } + return tl.DataVolumeClaimTemplate +} + +// HasLogVolumeClaimTemplate checks whether log volume claim template is specified +func (tl *TemplatesList) HasLogVolumeClaimTemplate() bool { + if tl == nil { + return false + } + return len(tl.LogVolumeClaimTemplate) > 0 +} + +// GetLogVolumeClaimTemplate gets log volume claim template +func (tl *TemplatesList) GetLogVolumeClaimTemplate() string { + if tl == nil { + return "" + } + return tl.LogVolumeClaimTemplate +} + +// HasServiceTemplate checks whether service template is specified +func (tl *TemplatesList) HasServiceTemplate() bool { + if tl == nil { + return false + } + return len(tl.ServiceTemplate) > 0 +} + +// HasServiceTemplates checks whether service template is specified +func (tl *TemplatesList) HasServiceTemplates() bool { + if tl == nil { + return false + } + return len(tl.ServiceTemplates) > 0 +} + +// HasAnyServiceTemplate checks whether any service template is specified +func (tl *TemplatesList) HasAnyServiceTemplate() bool { + if tl == nil { + return false + } + return tl.HasServiceTemplate() || tl.HasServiceTemplates() +} + +// GetServiceTemplate gets service template +func (tl *TemplatesList) GetServiceTemplate() string { + if tl == nil { + return "" + } + return tl.ServiceTemplate +} + +// GetServiceTemplates gets service templates +func (tl *TemplatesList) GetServiceTemplates() []string { + if tl == nil { + return nil + } + return tl.ServiceTemplates +} + +// GetAllServiceTemplates gets all service templates +func (tl *TemplatesList) GetAllServiceTemplates() []string { + if tl == nil { + return nil + } + var res []string + if tl.HasServiceTemplate() { + res = util.MergeStringArrays(res, []string{tl.GetServiceTemplate()}) + } + if tl.HasServiceTemplates() { + res = util.MergeStringArrays(res, tl.GetServiceTemplates()) + } + if len(res) > 0 { + return res + } + return nil +} + +// HasClusterServiceTemplate checks whether cluster service template is specified +func (tl *TemplatesList) HasClusterServiceTemplate() bool { + if tl == nil { + return false + } + return len(tl.ClusterServiceTemplate) > 0 +} + +// GetClusterServiceTemplate gets cluster service template +func (tl *TemplatesList) GetClusterServiceTemplate() string { + if tl == nil { + return "" + } + return tl.ClusterServiceTemplate +} + +// HasShardServiceTemplate checks whether shard service template is specified +func (tl *TemplatesList) HasShardServiceTemplate() bool { + if tl == nil { + return false + } + return len(tl.ShardServiceTemplate) > 0 +} + +// GetShardServiceTemplate gets shard service template +func (tl *TemplatesList) GetShardServiceTemplate() string { + if tl == nil { + return "" + } + return tl.ShardServiceTemplate +} + +// HasReplicaServiceTemplate checks whether replica service template is specified +func (tl *TemplatesList) HasReplicaServiceTemplate() bool { + if tl == nil { + return false + } + return len(tl.ReplicaServiceTemplate) > 0 +} + +// GetReplicaServiceTemplate gets replica service template +func (tl *TemplatesList) GetReplicaServiceTemplate() string { + if tl == nil { + return "" + } + return tl.ReplicaServiceTemplate +} + +// HandleDeprecatedFields helps to deal with deprecated fields +func (tl *TemplatesList) HandleDeprecatedFields() { + if tl == nil { + return + } + if tl.DataVolumeClaimTemplate == "" { + tl.DataVolumeClaimTemplate = tl.VolumeClaimTemplate + } +} + +// MergeFrom merges from specified object +func (tl *TemplatesList) MergeFrom(from *TemplatesList, _type MergeType) *TemplatesList { + if from == nil { + return tl + } + + if tl == nil { + tl = NewTemplatesList() + } + + switch _type { + case MergeTypeFillEmptyValues: + return tl.mergeFromFillEmptyValues(from) + case MergeTypeOverrideByNonEmptyValues: + return tl.mergeFromOverwriteByNonEmptyValues(from) + } + + return tl +} + +// mergeFromFillEmptyValues fills empty values +func (tl *TemplatesList) mergeFromFillEmptyValues(from *TemplatesList) *TemplatesList { + if tl.HostTemplate == "" { + tl.HostTemplate = from.HostTemplate + } + if tl.PodTemplate == "" { + tl.PodTemplate = from.PodTemplate + } + if tl.DataVolumeClaimTemplate == "" { + tl.DataVolumeClaimTemplate = from.DataVolumeClaimTemplate + } + if tl.LogVolumeClaimTemplate == "" { + tl.LogVolumeClaimTemplate = from.LogVolumeClaimTemplate + } + if tl.VolumeClaimTemplate == "" { + tl.VolumeClaimTemplate = from.VolumeClaimTemplate + } + if tl.ServiceTemplate == "" { + tl.ServiceTemplate = from.ServiceTemplate + } + tl.ServiceTemplates = util.MergeStringArrays(tl.ServiceTemplates, from.ServiceTemplates) + if tl.ClusterServiceTemplate == "" { + tl.ClusterServiceTemplate = from.ClusterServiceTemplate + } + if tl.ShardServiceTemplate == "" { + tl.ShardServiceTemplate = from.ShardServiceTemplate + } + if tl.ReplicaServiceTemplate == "" { + tl.ReplicaServiceTemplate = from.ReplicaServiceTemplate + } + return tl +} + +// mergeFromOverwriteByNonEmptyValues overwrites by non-empty values +func (tl *TemplatesList) mergeFromOverwriteByNonEmptyValues(from *TemplatesList) *TemplatesList { + if from.HostTemplate != "" { + tl.HostTemplate = from.HostTemplate + } + if from.PodTemplate != "" { + tl.PodTemplate = from.PodTemplate + } + if from.DataVolumeClaimTemplate != "" { + tl.DataVolumeClaimTemplate = from.DataVolumeClaimTemplate + } + if from.LogVolumeClaimTemplate != "" { + tl.LogVolumeClaimTemplate = from.LogVolumeClaimTemplate + } + if from.VolumeClaimTemplate != "" { + tl.VolumeClaimTemplate = from.VolumeClaimTemplate + } + if from.ServiceTemplate != "" { + tl.ServiceTemplate = from.ServiceTemplate + } + tl.ServiceTemplates = util.MergeStringArrays(tl.ServiceTemplates, from.ServiceTemplates) + if from.ClusterServiceTemplate != "" { + tl.ClusterServiceTemplate = from.ClusterServiceTemplate + } + if from.ShardServiceTemplate != "" { + tl.ShardServiceTemplate = from.ShardServiceTemplate + } + if from.ReplicaServiceTemplate != "" { + tl.ReplicaServiceTemplate = from.ReplicaServiceTemplate + } + return tl +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_volume_claim_template.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_volume_claim_template.go new file mode 100644 index 00000000..e898a679 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_volume_claim_template.go @@ -0,0 +1,112 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// VolumeClaimTemplate defines PersistentVolumeClaim Template +type VolumeClaimTemplate struct { + Name string `json:"name" yaml:"name"` + StorageManagement + ObjectMeta meta.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"` + Spec core.PersistentVolumeClaimSpec `json:"spec,omitempty" yaml:"spec,omitempty"` +} + +// PVCProvisioner defines PVC provisioner +type PVCProvisioner string + +// Possible values of PVC provisioner +const ( + PVCProvisionerUnspecified PVCProvisioner = "" + PVCProvisionerStatefulSet PVCProvisioner = "StatefulSet" + PVCProvisionerOperator PVCProvisioner = "Operator" +) + +// NewPVCProvisionerFromString creates new PVCProvisioner from string +func NewPVCProvisionerFromString(s string) PVCProvisioner { + return PVCProvisioner(s) +} + +// IsValid checks whether PVCProvisioner is valid +func (v PVCProvisioner) IsValid() bool { + switch v { + case + PVCProvisionerUnspecified, + PVCProvisionerStatefulSet, + PVCProvisionerOperator: + return true + } + return false +} + +// IsUnspecified checks whether PVCProvisioner is unspecified +func (v PVCProvisioner) IsUnspecified() bool { + return v == PVCProvisionerUnspecified +} + +// IsSpecified checks whether PVCProvisioner is specified +func (v PVCProvisioner) IsSpecified() bool { + return v.IsValid() && !v.IsUnspecified() +} + +// String returns string value for PVCProvisioner +func (v PVCProvisioner) String() string { + return string(v) +} + +// PVCReclaimPolicy defines PVC reclaim policy +type PVCReclaimPolicy string + +// Possible values of PVC reclaim policy +const ( + PVCReclaimPolicyUnspecified PVCReclaimPolicy = "" + PVCReclaimPolicyRetain PVCReclaimPolicy = "Retain" + PVCReclaimPolicyDelete PVCReclaimPolicy = "Delete" +) + +// NewPVCReclaimPolicyFromString creates new PVCReclaimPolicy from string +func NewPVCReclaimPolicyFromString(s string) PVCReclaimPolicy { + return PVCReclaimPolicy(s) +} + +// IsValid checks whether PVCReclaimPolicy is valid +func (v PVCReclaimPolicy) IsValid() bool { + switch v { + case + PVCReclaimPolicyUnspecified, + PVCReclaimPolicyRetain, + PVCReclaimPolicyDelete: + return true + } + return false +} + +// IsUnspecified checks whether PVCReclaimPolicy is unspecified +func (v PVCReclaimPolicy) IsUnspecified() bool { + return v == PVCReclaimPolicyUnspecified +} + +// IsSpecified checks whether PVCReclaimPolicy is specified +func (v PVCReclaimPolicy) IsSpecified() bool { + return v.IsValid() && !v.IsUnspecified() +} + +// String returns string value for PVCReclaimPolicy +func (v PVCReclaimPolicy) String() string { + return string(v) +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_zookeeper.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_zookeeper.go new file mode 100644 index 00000000..5aa2972c --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_zookeeper.go @@ -0,0 +1,130 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "strings" + + "gopkg.in/d4l3k/messagediff.v1" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +// ZookeeperConfig defines zookeeper section of .spec.configuration +// Refers to +// https://clickhouse.com/docs/operations/server-configuration-parameters/settings#zookeeper +type ZookeeperConfig struct { + Nodes ZookeeperNodes `json:"nodes,omitempty" yaml:"nodes,omitempty"` + SessionTimeoutMs int `json:"session_timeout_ms,omitempty" yaml:"session_timeout_ms,omitempty"` + OperationTimeoutMs int `json:"operation_timeout_ms,omitempty" yaml:"operation_timeout_ms,omitempty"` + Root string `json:"root,omitempty" yaml:"root,omitempty"` + Identity string `json:"identity,omitempty" yaml:"identity,omitempty"` + UseCompression *types.StringBool `json:"use_compression,omitempty" yaml:"use_compression,omitempty"` +} + +type ZookeeperNodes []ZookeeperNode + +func (n ZookeeperNodes) Len() int { + return len(n) +} + +func (n ZookeeperNodes) First() ZookeeperNode { + return n[0] +} + +func (n ZookeeperNodes) Servers() []string { + var servers []string + for _, node := range n { + servers = append(servers, node.String()) + } + return servers +} + +func (n ZookeeperNodes) String() string { + return strings.Join(n.Servers(), ",") +} + +// NewZookeeperConfig creates new ZookeeperConfig object +func NewZookeeperConfig() *ZookeeperConfig { + return new(ZookeeperConfig) +} + +// IsEmpty checks whether config is empty +func (zkc *ZookeeperConfig) IsEmpty() bool { + if zkc == nil { + return true + } + + return len(zkc.Nodes) == 0 +} + +// MergeFrom merges from provided object +func (zkc *ZookeeperConfig) MergeFrom(from *ZookeeperConfig, _type MergeType) *ZookeeperConfig { + if from == nil { + return zkc + } + + if zkc == nil { + zkc = NewZookeeperConfig() + } + + if !from.IsEmpty() { + // Append Nodes from `from` + if zkc.Nodes == nil { + zkc.Nodes = make([]ZookeeperNode, 0) + } + for fromIndex := range from.Nodes { + fromNode := &from.Nodes[fromIndex] + + // Try to find equal entry + equalFound := false + for toIndex := range zkc.Nodes { + toNode := &zkc.Nodes[toIndex] + if toNode.Equal(fromNode) { + // Received already have such a node + equalFound = true + break + } + } + + if !equalFound { + // Append Node from `from` + zkc.Nodes = append(zkc.Nodes, *fromNode.DeepCopy()) + } + } + } + + if from.SessionTimeoutMs > 0 { + zkc.SessionTimeoutMs = from.SessionTimeoutMs + } + if from.OperationTimeoutMs > 0 { + zkc.OperationTimeoutMs = from.OperationTimeoutMs + } + if from.Root != "" { + zkc.Root = from.Root + } + if from.Identity != "" { + zkc.Identity = from.Identity + } + zkc.UseCompression = zkc.UseCompression.MergeFrom(from.UseCompression) + + return zkc +} + +// Equals checks whether config is equal to another one +func (zkc *ZookeeperConfig) Equals(b *ZookeeperConfig) bool { + _, equals := messagediff.DeepDiff(zkc, b) + return equals +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_zookeeper_node.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_zookeeper_node.go new file mode 100644 index 00000000..7aaa9552 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/type_zookeeper_node.go @@ -0,0 +1,72 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" +) + +// ZookeeperNode defines item of nodes section of .spec.configuration.zookeeper +type ZookeeperNode struct { + Host string `json:"host,omitempty" yaml:"host,omitempty"` + Port *types.Int32 `json:"port,omitempty" yaml:"port,omitempty"` + Secure *types.StringBool `json:"secure,omitempty" yaml:"secure,omitempty"` + AvailabilityZone *types.String `json:"availabilityZone,omitempty" yaml:"availabilityZone,omitempty"` +} + +func (zkNode *ZookeeperNode) String() string { + if zkNode == nil { + return "" + } + str := zkNode.Host + if zkNode.Port.HasValue() { + str += ":" + zkNode.Port.String() + } + return str +} + +// Equal checks whether zookeeper node is equal to another +func (zkNode *ZookeeperNode) Equal(to *ZookeeperNode) bool { + if to == nil { + return false + } + + return zkNode.hostEqual(to) && zkNode.portEqual(to) && zkNode.secureEqual(to) && zkNode.availabilityZoneEqual(to) +} + +func (zkNode *ZookeeperNode) hostEqual(to *ZookeeperNode) bool { + return zkNode.Host == to.Host +} + +func (zkNode *ZookeeperNode) portEqual(to *ZookeeperNode) bool { + return zkNode.Port.Equal(to.Port) +} + +func (zkNode *ZookeeperNode) secureEqual(to *ZookeeperNode) bool { + return zkNode.Secure.Value() == to.Secure.Value() +} + +func (zkNode *ZookeeperNode) availabilityZoneEqual(to *ZookeeperNode) bool { + return zkNode.AvailabilityZone.Value() == to.AvailabilityZone.Value() +} + +// IsSecure checks whether zookeeper node is secure +func (zkNode *ZookeeperNode) IsSecure() bool { + if zkNode == nil { + return false + } + + return zkNode.Secure.Value() +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/types.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/types.go new file mode 100644 index 00000000..fd94bc8e --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/types.go @@ -0,0 +1,120 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "sync" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/swversion" +) + +// MergeType specifies merge types type +type MergeType string + +// Possible merge types +const ( + MergeTypeFillEmptyValues MergeType = "fillempty" + MergeTypeOverrideByNonEmptyValues MergeType = "override" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClickHouseInstallation defines the Installation of a ClickHouse Database Cluster +type ClickHouseInstallation struct { + meta.TypeMeta `json:",inline" yaml:",inline"` + meta.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"` + + Spec ChiSpec `json:"spec" yaml:"spec"` + Status *Status `json:"status,omitempty" yaml:"status,omitempty"` + + runtime *ClickHouseInstallationRuntime `json:"-" yaml:"-"` + statusCreatorMutex sync.Mutex `json:"-" yaml:"-"` + runtimeCreatorMutex sync.Mutex `json:"-" yaml:"-"` +} + +type ClickHouseInstallationRuntime struct { + attributes *ComparableAttributes `json:"-" yaml:"-"` + commonConfigMutex sync.Mutex `json:"-" yaml:"-"` + MinVersion *swversion.SoftWareVersion `json:"-" yaml:"-"` + MaxVersion *swversion.SoftWareVersion `json:"-" yaml:"-"` + ActionPlan IActionPlan `json:"-" yaml:"-"` +} + +func newClickHouseInstallationRuntime() *ClickHouseInstallationRuntime { + return &ClickHouseInstallationRuntime{ + attributes: &ComparableAttributes{}, + } +} + +func (runtime *ClickHouseInstallationRuntime) GetAttributes() *ComparableAttributes { + return runtime.attributes +} + +func (runtime *ClickHouseInstallationRuntime) LockCommonConfig() { + runtime.commonConfigMutex.Lock() +} + +func (runtime *ClickHouseInstallationRuntime) UnlockCommonConfig() { + runtime.commonConfigMutex.Unlock() +} + +// +genclient +// +genclient:noStatus +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClickHouseInstallationTemplate defines ClickHouseInstallation template +type ClickHouseInstallationTemplate ClickHouseInstallation + +// +genclient +// +genclient:noStatus +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClickHouseOperatorConfiguration defines CHOp config +type ClickHouseOperatorConfiguration struct { + meta.TypeMeta `json:",inline" yaml:",inline"` + meta.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"` + Spec OperatorConfig `json:"spec" yaml:"spec"` + Status string `json:"status" yaml:"status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClickHouseInstallationList defines a list of ClickHouseInstallation resources +type ClickHouseInstallationList struct { + meta.TypeMeta `json:",inline" yaml:",inline"` + meta.ListMeta `json:"metadata" yaml:"metadata"` + Items []ClickHouseInstallation `json:"items" yaml:"items"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClickHouseInstallationTemplateList defines CHI template list +type ClickHouseInstallationTemplateList struct { + meta.TypeMeta `json:",inline" yaml:",inline"` + meta.ListMeta `json:"metadata" yaml:"metadata"` + Items []ClickHouseInstallationTemplate `json:"items" yaml:"items"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClickHouseOperatorConfigurationList defines CHI operator config list +type ClickHouseOperatorConfigurationList struct { + meta.TypeMeta `json:",inline" yaml:",inline"` + meta.ListMeta `json:"metadata" yaml:"metadata"` + Items []ClickHouseOperatorConfiguration `json:"items" yaml:"items"` +} diff --git a/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/zz_generated.deepcopy.go b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/zz_generated.deepcopy.go new file mode 100644 index 00000000..7d1f9cba --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1/zz_generated.deepcopy.go @@ -0,0 +1,3036 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1 + +import ( + types "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" + util "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" + messagediffv1 "gopkg.in/d4l3k/messagediff.v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActionPlan) DeepCopyInto(out *ActionPlan) { + *out = *in + if in.old != nil { + out.old = in.old + } + if in.new != nil { + out.new = in.new + } + if in.specDiff != nil { + in, out := &in.specDiff, &out.specDiff + *out = new(messagediffv1.Diff) + *in = *out + } + if in.specDiffReverse != nil { + in, out := &in.specDiffReverse, &out.specDiffReverse + *out = new(messagediffv1.Diff) + *in = *out + } + if in.labelsDiff != nil { + in, out := &in.labelsDiff, &out.labelsDiff + *out = new(messagediffv1.Diff) + *in = *out + } + if in.deletionTimestampDiff != nil { + in, out := &in.deletionTimestampDiff, &out.deletionTimestampDiff + *out = new(messagediffv1.Diff) + *in = *out + } + if in.finalizersDiff != nil { + in, out := &in.finalizersDiff, &out.finalizersDiff + *out = new(messagediffv1.Diff) + *in = *out + } + if in.attributesDiff != nil { + in, out := &in.attributesDiff, &out.attributesDiff + *out = new(messagediffv1.Diff) + *in = *out + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActionPlan. +func (in *ActionPlan) DeepCopy() *ActionPlan { + if in == nil { + return nil + } + out := new(ActionPlan) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AddonConfiguration) DeepCopyInto(out *AddonConfiguration) { + *out = *in + if in.Users != nil { + in, out := &in.Users, &out.Users + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Profiles != nil { + in, out := &in.Profiles, &out.Profiles + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Quotas != nil { + in, out := &in.Quotas, &out.Quotas + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddonConfiguration. +func (in *AddonConfiguration) DeepCopy() *AddonConfiguration { + if in == nil { + return nil + } + out := new(AddonConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AddonSpec) DeepCopyInto(out *AddonSpec) { + *out = *in + if in.Configuration != nil { + in, out := &in.Configuration, &out.Configuration + *out = new(AddonConfiguration) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddonSpec. +func (in *AddonSpec) DeepCopy() *AddonSpec { + if in == nil { + return nil + } + out := new(AddonSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChiClusterAddress) DeepCopyInto(out *ChiClusterAddress) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChiClusterAddress. +func (in *ChiClusterAddress) DeepCopy() *ChiClusterAddress { + if in == nil { + return nil + } + out := new(ChiClusterAddress) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChiClusterLayout) DeepCopyInto(out *ChiClusterLayout) { + *out = *in + if in.Shards != nil { + in, out := &in.Shards, &out.Shards + *out = make([]*ChiShard, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(ChiShard) + (*in).DeepCopyInto(*out) + } + } + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = make([]*ChiReplica, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(ChiReplica) + (*in).DeepCopyInto(*out) + } + } + } + if in.HostsField != nil { + in, out := &in.HostsField, &out.HostsField + *out = new(HostsField) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChiClusterLayout. +func (in *ChiClusterLayout) DeepCopy() *ChiClusterLayout { + if in == nil { + return nil + } + out := new(ChiClusterLayout) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChiClusterRuntime) DeepCopyInto(out *ChiClusterRuntime) { + *out = *in + out.Address = in.Address + if in.CHI != nil { + in, out := &in.CHI, &out.CHI + *out = new(ClickHouseInstallation) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChiClusterRuntime. +func (in *ChiClusterRuntime) DeepCopy() *ChiClusterRuntime { + if in == nil { + return nil + } + out := new(ChiClusterRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChiReconcile) DeepCopyInto(out *ChiReconcile) { + *out = *in + if in.Cleanup != nil { + in, out := &in.Cleanup, &out.Cleanup + *out = new(Cleanup) + (*in).DeepCopyInto(*out) + } + in.Macros.DeepCopyInto(&out.Macros) + out.Runtime = in.Runtime + in.Host.DeepCopyInto(&out.Host) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChiReconcile. +func (in *ChiReconcile) DeepCopy() *ChiReconcile { + if in == nil { + return nil + } + out := new(ChiReconcile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChiReplica) DeepCopyInto(out *ChiReplica) { + *out = *in + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = new(Settings) + (*in).DeepCopyInto(*out) + } + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = new(Settings) + (*in).DeepCopyInto(*out) + } + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = new(TemplatesList) + (*in).DeepCopyInto(*out) + } + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]*Host, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Host) + (*in).DeepCopyInto(*out) + } + } + } + in.Runtime.DeepCopyInto(&out.Runtime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChiReplica. +func (in *ChiReplica) DeepCopy() *ChiReplica { + if in == nil { + return nil + } + out := new(ChiReplica) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChiReplicaAddress) DeepCopyInto(out *ChiReplicaAddress) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChiReplicaAddress. +func (in *ChiReplicaAddress) DeepCopy() *ChiReplicaAddress { + if in == nil { + return nil + } + out := new(ChiReplicaAddress) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChiReplicaRuntime) DeepCopyInto(out *ChiReplicaRuntime) { + *out = *in + out.Address = in.Address + if in.CHI != nil { + in, out := &in.CHI, &out.CHI + *out = new(ClickHouseInstallation) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChiReplicaRuntime. +func (in *ChiReplicaRuntime) DeepCopy() *ChiReplicaRuntime { + if in == nil { + return nil + } + out := new(ChiReplicaRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChiShard) DeepCopyInto(out *ChiShard) { + *out = *in + if in.Weight != nil { + in, out := &in.Weight, &out.Weight + *out = new(int) + **out = **in + } + if in.InternalReplication != nil { + in, out := &in.InternalReplication, &out.InternalReplication + *out = new(types.StringBool) + **out = **in + } + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = new(Settings) + (*in).DeepCopyInto(*out) + } + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = new(Settings) + (*in).DeepCopyInto(*out) + } + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = new(TemplatesList) + (*in).DeepCopyInto(*out) + } + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]*Host, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Host) + (*in).DeepCopyInto(*out) + } + } + } + in.Runtime.DeepCopyInto(&out.Runtime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChiShard. +func (in *ChiShard) DeepCopy() *ChiShard { + if in == nil { + return nil + } + out := new(ChiShard) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChiShardAddress) DeepCopyInto(out *ChiShardAddress) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChiShardAddress. +func (in *ChiShardAddress) DeepCopy() *ChiShardAddress { + if in == nil { + return nil + } + out := new(ChiShardAddress) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChiShardRuntime) DeepCopyInto(out *ChiShardRuntime) { + *out = *in + out.Address = in.Address + if in.CHI != nil { + in, out := &in.CHI, &out.CHI + *out = new(ClickHouseInstallation) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChiShardRuntime. +func (in *ChiShardRuntime) DeepCopy() *ChiShardRuntime { + if in == nil { + return nil + } + out := new(ChiShardRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChiSpec) DeepCopyInto(out *ChiSpec) { + *out = *in + if in.TaskID != nil { + in, out := &in.TaskID, &out.TaskID + *out = new(types.Id) + **out = **in + } + if in.Stop != nil { + in, out := &in.Stop, &out.Stop + *out = new(types.StringBool) + **out = **in + } + if in.Restart != nil { + in, out := &in.Restart, &out.Restart + *out = new(types.String) + **out = **in + } + if in.Troubleshoot != nil { + in, out := &in.Troubleshoot, &out.Troubleshoot + *out = new(types.StringBool) + **out = **in + } + if in.Suspend != nil { + in, out := &in.Suspend, &out.Suspend + *out = new(types.StringBool) + **out = **in + } + if in.NamespaceDomainPattern != nil { + in, out := &in.NamespaceDomainPattern, &out.NamespaceDomainPattern + *out = new(types.String) + **out = **in + } + if in.Templating != nil { + in, out := &in.Templating, &out.Templating + *out = new(ChiTemplating) + (*in).DeepCopyInto(*out) + } + if in.Reconciling != nil { + in, out := &in.Reconciling, &out.Reconciling + *out = new(ChiReconcile) + (*in).DeepCopyInto(*out) + } + if in.Reconcile != nil { + in, out := &in.Reconcile, &out.Reconcile + *out = new(ChiReconcile) + (*in).DeepCopyInto(*out) + } + if in.Defaults != nil { + in, out := &in.Defaults, &out.Defaults + *out = new(Defaults) + (*in).DeepCopyInto(*out) + } + if in.Configuration != nil { + in, out := &in.Configuration, &out.Configuration + *out = new(Configuration) + (*in).DeepCopyInto(*out) + } + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = new(Templates) + (*in).DeepCopyInto(*out) + } + if in.UseTemplates != nil { + in, out := &in.UseTemplates, &out.UseTemplates + *out = make([]*TemplateRef, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(TemplateRef) + **out = **in + } + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChiSpec. +func (in *ChiSpec) DeepCopy() *ChiSpec { + if in == nil { + return nil + } + out := new(ChiSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChiTemplating) DeepCopyInto(out *ChiTemplating) { + *out = *in + if in.CHISelector != nil { + in, out := &in.CHISelector, &out.CHISelector + *out = make(TargetSelector, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChiTemplating. +func (in *ChiTemplating) DeepCopy() *ChiTemplating { + if in == nil { + return nil + } + out := new(ChiTemplating) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Cleanup) DeepCopyInto(out *Cleanup) { + *out = *in + if in.UnknownObjects != nil { + in, out := &in.UnknownObjects, &out.UnknownObjects + *out = new(ObjectsCleanup) + **out = **in + } + if in.ReconcileFailedObjects != nil { + in, out := &in.ReconcileFailedObjects, &out.ReconcileFailedObjects + *out = new(ObjectsCleanup) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cleanup. +func (in *Cleanup) DeepCopy() *Cleanup { + if in == nil { + return nil + } + out := new(Cleanup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseInstallation) DeepCopyInto(out *ClickHouseInstallation) { + // *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = new(Status) + (*in).DeepCopyInto(*out) + } + if in.runtime != nil { + in, out := &in.runtime, &out.runtime + *out = new(ClickHouseInstallationRuntime) + (*in).DeepCopyInto(*out) + } + // out.statusCreatorMutex = in.statusCreatorMutex + // out.runtimeCreatorMutex = in.runtimeCreatorMutex + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseInstallation. +func (in *ClickHouseInstallation) DeepCopy() *ClickHouseInstallation { + if in == nil { + return nil + } + out := new(ClickHouseInstallation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClickHouseInstallation) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseInstallationList) DeepCopyInto(out *ClickHouseInstallationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClickHouseInstallation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseInstallationList. +func (in *ClickHouseInstallationList) DeepCopy() *ClickHouseInstallationList { + if in == nil { + return nil + } + out := new(ClickHouseInstallationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClickHouseInstallationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseInstallationRuntime) DeepCopyInto(out *ClickHouseInstallationRuntime) { + // *out = *in + if in.attributes != nil { + in, out := &in.attributes, &out.attributes + *out = new(ComparableAttributes) + (*in).DeepCopyInto(*out) + } + // out.commonConfigMutex = in.commonConfigMutex + if in.MinVersion != nil { + in, out := &in.MinVersion, &out.MinVersion + *out = (*in).DeepCopy() + } + if in.MaxVersion != nil { + in, out := &in.MaxVersion, &out.MaxVersion + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseInstallationRuntime. +func (in *ClickHouseInstallationRuntime) DeepCopy() *ClickHouseInstallationRuntime { + if in == nil { + return nil + } + out := new(ClickHouseInstallationRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseInstallationTemplate) DeepCopyInto(out *ClickHouseInstallationTemplate) { + // *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = new(Status) + (*in).DeepCopyInto(*out) + } + if in.runtime != nil { + in, out := &in.runtime, &out.runtime + *out = new(ClickHouseInstallationRuntime) + (*in).DeepCopyInto(*out) + } + // out.statusCreatorMutex = in.statusCreatorMutex + // out.runtimeCreatorMutex = in.runtimeCreatorMutex + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseInstallationTemplate. +func (in *ClickHouseInstallationTemplate) DeepCopy() *ClickHouseInstallationTemplate { + if in == nil { + return nil + } + out := new(ClickHouseInstallationTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClickHouseInstallationTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseInstallationTemplateList) DeepCopyInto(out *ClickHouseInstallationTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClickHouseInstallationTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseInstallationTemplateList. +func (in *ClickHouseInstallationTemplateList) DeepCopy() *ClickHouseInstallationTemplateList { + if in == nil { + return nil + } + out := new(ClickHouseInstallationTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClickHouseInstallationTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseOperatorConfiguration) DeepCopyInto(out *ClickHouseOperatorConfiguration) { + // *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseOperatorConfiguration. +func (in *ClickHouseOperatorConfiguration) DeepCopy() *ClickHouseOperatorConfiguration { + if in == nil { + return nil + } + out := new(ClickHouseOperatorConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClickHouseOperatorConfiguration) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClickHouseOperatorConfigurationList) DeepCopyInto(out *ClickHouseOperatorConfigurationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClickHouseOperatorConfiguration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClickHouseOperatorConfigurationList. +func (in *ClickHouseOperatorConfigurationList) DeepCopy() *ClickHouseOperatorConfigurationList { + if in == nil { + return nil + } + out := new(ClickHouseOperatorConfigurationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClickHouseOperatorConfigurationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Cluster) DeepCopyInto(out *Cluster) { + *out = *in + if in.Zookeeper != nil { + in, out := &in.Zookeeper, &out.Zookeeper + *out = new(ZookeeperConfig) + (*in).DeepCopyInto(*out) + } + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = new(Settings) + (*in).DeepCopyInto(*out) + } + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = new(Settings) + (*in).DeepCopyInto(*out) + } + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = new(TemplatesList) + (*in).DeepCopyInto(*out) + } + if in.SchemaPolicy != nil { + in, out := &in.SchemaPolicy, &out.SchemaPolicy + *out = new(SchemaPolicy) + **out = **in + } + if in.Insecure != nil { + in, out := &in.Insecure, &out.Insecure + *out = new(types.StringBool) + **out = **in + } + if in.Secure != nil { + in, out := &in.Secure, &out.Secure + *out = new(types.StringBool) + **out = **in + } + if in.Secret != nil { + in, out := &in.Secret, &out.Secret + *out = new(ClusterSecret) + (*in).DeepCopyInto(*out) + } + if in.PDBManaged != nil { + in, out := &in.PDBManaged, &out.PDBManaged + *out = new(types.StringBool) + **out = **in + } + if in.PDBMaxUnavailable != nil { + in, out := &in.PDBMaxUnavailable, &out.PDBMaxUnavailable + *out = new(types.Int32) + **out = **in + } + if in.Reconcile != nil { + in, out := &in.Reconcile, &out.Reconcile + *out = new(ClusterReconcile) + (*in).DeepCopyInto(*out) + } + if in.Layout != nil { + in, out := &in.Layout, &out.Layout + *out = new(ChiClusterLayout) + (*in).DeepCopyInto(*out) + } + in.Runtime.DeepCopyInto(&out.Runtime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cluster. +func (in *Cluster) DeepCopy() *Cluster { + if in == nil { + return nil + } + out := new(Cluster) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterReconcile) DeepCopyInto(out *ClusterReconcile) { + *out = *in + out.Runtime = in.Runtime + in.Host.DeepCopyInto(&out.Host) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterReconcile. +func (in *ClusterReconcile) DeepCopy() *ClusterReconcile { + if in == nil { + return nil + } + out := new(ClusterReconcile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterSecret) DeepCopyInto(out *ClusterSecret) { + *out = *in + if in.Auto != nil { + in, out := &in.Auto, &out.Auto + *out = new(types.StringBool) + **out = **in + } + if in.ValueFrom != nil { + in, out := &in.ValueFrom, &out.ValueFrom + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSecret. +func (in *ClusterSecret) DeepCopy() *ClusterSecret { + if in == nil { + return nil + } + out := new(ClusterSecret) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComparableAttributes) DeepCopyInto(out *ComparableAttributes) { + *out = *in + if in.additionalEnvVars != nil { + in, out := &in.additionalEnvVars, &out.additionalEnvVars + *out = make([]corev1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.additionalVolumes != nil { + in, out := &in.additionalVolumes, &out.additionalVolumes + *out = make([]corev1.Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.additionalVolumeMounts != nil { + in, out := &in.additionalVolumeMounts, &out.additionalVolumeMounts + *out = make([]corev1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComparableAttributes. +func (in *ComparableAttributes) DeepCopy() *ComparableAttributes { + if in == nil { + return nil + } + out := new(ComparableAttributes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigCRSource) DeepCopyInto(out *ConfigCRSource) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigCRSource. +func (in *ConfigCRSource) DeepCopy() *ConfigCRSource { + if in == nil { + return nil + } + out := new(ConfigCRSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Configuration) DeepCopyInto(out *Configuration) { + *out = *in + if in.Zookeeper != nil { + in, out := &in.Zookeeper, &out.Zookeeper + *out = new(ZookeeperConfig) + (*in).DeepCopyInto(*out) + } + if in.Users != nil { + in, out := &in.Users, &out.Users + *out = new(Settings) + (*in).DeepCopyInto(*out) + } + if in.Profiles != nil { + in, out := &in.Profiles, &out.Profiles + *out = new(Settings) + (*in).DeepCopyInto(*out) + } + if in.Quotas != nil { + in, out := &in.Quotas, &out.Quotas + *out = new(Settings) + (*in).DeepCopyInto(*out) + } + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = new(Settings) + (*in).DeepCopyInto(*out) + } + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = new(Settings) + (*in).DeepCopyInto(*out) + } + if in.Clusters != nil { + in, out := &in.Clusters, &out.Clusters + *out = make([]*Cluster, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Cluster) + (*in).DeepCopyInto(*out) + } + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration. +func (in *Configuration) DeepCopy() *Configuration { + if in == nil { + return nil + } + out := new(Configuration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Defaults) DeepCopyInto(out *Defaults) { + *out = *in + if in.ReplicasUseFQDN != nil { + in, out := &in.ReplicasUseFQDN, &out.ReplicasUseFQDN + *out = new(types.StringBool) + **out = **in + } + if in.DistributedDDL != nil { + in, out := &in.DistributedDDL, &out.DistributedDDL + *out = new(DistributedDDL) + **out = **in + } + if in.StorageManagement != nil { + in, out := &in.StorageManagement, &out.StorageManagement + *out = new(StorageManagement) + **out = **in + } + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = new(TemplatesList) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Defaults. +func (in *Defaults) DeepCopy() *Defaults { + if in == nil { + return nil + } + out := new(Defaults) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DistributedDDL) DeepCopyInto(out *DistributedDDL) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DistributedDDL. +func (in *DistributedDDL) DeepCopy() *DistributedDDL { + if in == nil { + return nil + } + out := new(DistributedDDL) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FillStatusParams) DeepCopyInto(out *FillStatusParams) { + *out = *in + if in.Pods != nil { + in, out := &in.Pods, &out.Pods + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.FQDNs != nil { + in, out := &in.FQDNs, &out.FQDNs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Endpoints != nil { + in, out := &in.Endpoints, &out.Endpoints + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.NormalizedCR != nil { + in, out := &in.NormalizedCR, &out.NormalizedCR + *out = new(ClickHouseInstallation) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FillStatusParams. +func (in *FillStatusParams) DeepCopy() *FillStatusParams { + if in == nil { + return nil + } + out := new(FillStatusParams) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Host) DeepCopyInto(out *Host) { + *out = *in + in.HostSecure.DeepCopyInto(&out.HostSecure) + in.HostPorts.DeepCopyInto(&out.HostPorts) + in.HostSettings.DeepCopyInto(&out.HostSettings) + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = new(TemplatesList) + (*in).DeepCopyInto(*out) + } + in.Runtime.DeepCopyInto(&out.Runtime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Host. +func (in *Host) DeepCopy() *Host { + if in == nil { + return nil + } + out := new(Host) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostAddress) DeepCopyInto(out *HostAddress) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAddress. +func (in *HostAddress) DeepCopy() *HostAddress { + if in == nil { + return nil + } + out := new(HostAddress) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostPorts) DeepCopyInto(out *HostPorts) { + *out = *in + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(types.Int32) + **out = **in + } + if in.TCPPort != nil { + in, out := &in.TCPPort, &out.TCPPort + *out = new(types.Int32) + **out = **in + } + if in.TLSPort != nil { + in, out := &in.TLSPort, &out.TLSPort + *out = new(types.Int32) + **out = **in + } + if in.HTTPPort != nil { + in, out := &in.HTTPPort, &out.HTTPPort + *out = new(types.Int32) + **out = **in + } + if in.HTTPSPort != nil { + in, out := &in.HTTPSPort, &out.HTTPSPort + *out = new(types.Int32) + **out = **in + } + if in.InterserverHTTPPort != nil { + in, out := &in.InterserverHTTPPort, &out.InterserverHTTPPort + *out = new(types.Int32) + **out = **in + } + if in.ZKPort != nil { + in, out := &in.ZKPort, &out.ZKPort + *out = new(types.Int32) + **out = **in + } + if in.RaftPort != nil { + in, out := &in.RaftPort, &out.RaftPort + *out = new(types.Int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostPorts. +func (in *HostPorts) DeepCopy() *HostPorts { + if in == nil { + return nil + } + out := new(HostPorts) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostRuntime) DeepCopyInto(out *HostRuntime) { + *out = *in + out.Address = in.Address + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = (*in).DeepCopy() + } + if in.reconcileAttributes != nil { + in, out := &in.reconcileAttributes, &out.reconcileAttributes + *out = (*in).DeepCopy() + } + if in.replicas != nil { + in, out := &in.replicas, &out.replicas + *out = new(types.Int32) + **out = **in + } + if in.CurStatefulSet != nil { + in, out := &in.CurStatefulSet, &out.CurStatefulSet + *out = new(appsv1.StatefulSet) + (*in).DeepCopyInto(*out) + } + if in.DesiredStatefulSet != nil { + in, out := &in.DesiredStatefulSet, &out.DesiredStatefulSet + *out = new(appsv1.StatefulSet) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostRuntime. +func (in *HostRuntime) DeepCopy() *HostRuntime { + if in == nil { + return nil + } + out := new(HostRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostSecure) DeepCopyInto(out *HostSecure) { + *out = *in + if in.Insecure != nil { + in, out := &in.Insecure, &out.Insecure + *out = new(types.StringBool) + **out = **in + } + if in.Secure != nil { + in, out := &in.Secure, &out.Secure + *out = new(types.StringBool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostSecure. +func (in *HostSecure) DeepCopy() *HostSecure { + if in == nil { + return nil + } + out := new(HostSecure) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostSettings) DeepCopyInto(out *HostSettings) { + *out = *in + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = new(Settings) + (*in).DeepCopyInto(*out) + } + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = new(Settings) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostSettings. +func (in *HostSettings) DeepCopy() *HostSettings { + if in == nil { + return nil + } + out := new(HostSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostTemplate) DeepCopyInto(out *HostTemplate) { + *out = *in + if in.PortDistribution != nil { + in, out := &in.PortDistribution, &out.PortDistribution + *out = make([]PortDistribution, len(*in)) + copy(*out, *in) + } + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostTemplate. +func (in *HostTemplate) DeepCopy() *HostTemplate { + if in == nil { + return nil + } + out := new(HostTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostTemplatesIndex) DeepCopyInto(out *HostTemplatesIndex) { + *out = *in + if in.templates != nil { + in, out := &in.templates, &out.templates + *out = make(map[string]*HostTemplate, len(*in)) + for key, val := range *in { + var outVal *HostTemplate + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = new(HostTemplate) + (*in).DeepCopyInto(*out) + } + (*out)[key] = outVal + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostTemplatesIndex. +func (in *HostTemplatesIndex) DeepCopy() *HostTemplatesIndex { + if in == nil { + return nil + } + out := new(HostTemplatesIndex) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostsField) DeepCopyInto(out *HostsField) { + *out = *in + if in.Field != nil { + in, out := &in.Field, &out.Field + *out = make([][]*Host, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make([]*Host, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Host) + (*in).DeepCopyInto(*out) + } + } + } + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostsField. +func (in *HostsField) DeepCopy() *HostsField { + if in == nil { + return nil + } + out := new(HostsField) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MacrosSection) DeepCopyInto(out *MacrosSection) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(types.StringBool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MacrosSection. +func (in *MacrosSection) DeepCopy() *MacrosSection { + if in == nil { + return nil + } + out := new(MacrosSection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MacrosSections) DeepCopyInto(out *MacrosSections) { + *out = *in + in.Users.DeepCopyInto(&out.Users) + in.Profiles.DeepCopyInto(&out.Profiles) + in.Quotas.DeepCopyInto(&out.Quotas) + in.Settings.DeepCopyInto(&out.Settings) + in.Files.DeepCopyInto(&out.Files) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MacrosSections. +func (in *MacrosSections) DeepCopy() *MacrosSections { + if in == nil { + return nil + } + out := new(MacrosSections) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectsCleanup) DeepCopyInto(out *ObjectsCleanup) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectsCleanup. +func (in *ObjectsCleanup) DeepCopy() *ObjectsCleanup { + if in == nil { + return nil + } + out := new(ObjectsCleanup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfig) DeepCopyInto(out *OperatorConfig) { + // *out = *in + in.Runtime.DeepCopyInto(&out.Runtime) + in.Watch.DeepCopyInto(&out.Watch) + in.ClickHouse.DeepCopyInto(&out.ClickHouse) + in.Keeper.DeepCopyInto(&out.Keeper) + in.Template.DeepCopyInto(&out.Template) + in.Reconcile.DeepCopyInto(&out.Reconcile) + in.Annotation.DeepCopyInto(&out.Annotation) + in.Label.DeepCopyInto(&out.Label) + in.Metrics.DeepCopyInto(&out.Metrics) + in.Status.DeepCopyInto(&out.Status) + out.StatefulSet = in.StatefulSet + out.Pod = in.Pod + out.Logger = in.Logger + if in.WatchNamespaces != nil { + in, out := &in.WatchNamespaces, &out.WatchNamespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.CHConfigUserDefaultNetworksIP != nil { + in, out := &in.CHConfigUserDefaultNetworksIP, &out.CHConfigUserDefaultNetworksIP + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.IncludeIntoPropagationAnnotations != nil { + in, out := &in.IncludeIntoPropagationAnnotations, &out.IncludeIntoPropagationAnnotations + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ExcludeFromPropagationAnnotations != nil { + in, out := &in.ExcludeFromPropagationAnnotations, &out.ExcludeFromPropagationAnnotations + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.IncludeIntoPropagationLabels != nil { + in, out := &in.IncludeIntoPropagationLabels, &out.IncludeIntoPropagationLabels + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ExcludeFromPropagationLabels != nil { + in, out := &in.ExcludeFromPropagationLabels, &out.ExcludeFromPropagationLabels + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfig. +func (in *OperatorConfig) DeepCopy() *OperatorConfig { + if in == nil { + return nil + } + out := new(OperatorConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigAddonRule) DeepCopyInto(out *OperatorConfigAddonRule) { + *out = *in + if in.Spec != nil { + in, out := &in.Spec, &out.Spec + *out = new(AddonSpec) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigAddonRule. +func (in *OperatorConfigAddonRule) DeepCopy() *OperatorConfigAddonRule { + if in == nil { + return nil + } + out := new(OperatorConfigAddonRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigAddons) DeepCopyInto(out *OperatorConfigAddons) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]OperatorConfigAddonRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigAddons. +func (in *OperatorConfigAddons) DeepCopy() *OperatorConfigAddons { + if in == nil { + return nil + } + out := new(OperatorConfigAddons) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigAnnotation) DeepCopyInto(out *OperatorConfigAnnotation) { + *out = *in + if in.Include != nil { + in, out := &in.Include, &out.Include + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Exclude != nil { + in, out := &in.Exclude, &out.Exclude + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigAnnotation. +func (in *OperatorConfigAnnotation) DeepCopy() *OperatorConfigAnnotation { + if in == nil { + return nil + } + out := new(OperatorConfigAnnotation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigCHI) DeepCopyInto(out *OperatorConfigCHI) { + // *out = *in + in.Runtime.DeepCopyInto(&out.Runtime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigCHI. +func (in *OperatorConfigCHI) DeepCopy() *OperatorConfigCHI { + if in == nil { + return nil + } + out := new(OperatorConfigCHI) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigCHIRuntime) DeepCopyInto(out *OperatorConfigCHIRuntime) { + // *out = *in + if in.TemplateFiles != nil { + in, out := &in.TemplateFiles, &out.TemplateFiles + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = make([]*ClickHouseInstallation, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(ClickHouseInstallation) + (*in).DeepCopyInto(*out) + } + } + } + // out.mutex = in.mutex + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigCHIRuntime. +func (in *OperatorConfigCHIRuntime) DeepCopy() *OperatorConfigCHIRuntime { + if in == nil { + return nil + } + out := new(OperatorConfigCHIRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigClickHouse) DeepCopyInto(out *OperatorConfigClickHouse) { + *out = *in + in.Config.DeepCopyInto(&out.Config) + in.ConfigRestartPolicy.DeepCopyInto(&out.ConfigRestartPolicy) + out.Access = in.Access + in.Addons.DeepCopyInto(&out.Addons) + out.Metrics = in.Metrics + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigClickHouse. +func (in *OperatorConfigClickHouse) DeepCopy() *OperatorConfigClickHouse { + if in == nil { + return nil + } + out := new(OperatorConfigClickHouse) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigConfig) DeepCopyInto(out *OperatorConfigConfig) { + *out = *in + in.File.DeepCopyInto(&out.File) + in.User.DeepCopyInto(&out.User) + out.Network = in.Network + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigConfig. +func (in *OperatorConfigConfig) DeepCopy() *OperatorConfigConfig { + if in == nil { + return nil + } + out := new(OperatorConfigConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigDefault) DeepCopyInto(out *OperatorConfigDefault) { + *out = *in + if in.NetworksIP != nil { + in, out := &in.NetworksIP, &out.NetworksIP + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigDefault. +func (in *OperatorConfigDefault) DeepCopy() *OperatorConfigDefault { + if in == nil { + return nil + } + out := new(OperatorConfigDefault) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigFile) DeepCopyInto(out *OperatorConfigFile) { + *out = *in + out.Path = in.Path + in.Runtime.DeepCopyInto(&out.Runtime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigFile. +func (in *OperatorConfigFile) DeepCopy() *OperatorConfigFile { + if in == nil { + return nil + } + out := new(OperatorConfigFile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigFileRuntime) DeepCopyInto(out *OperatorConfigFileRuntime) { + *out = *in + if in.CommonConfigFiles != nil { + in, out := &in.CommonConfigFiles, &out.CommonConfigFiles + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.HostConfigFiles != nil { + in, out := &in.HostConfigFiles, &out.HostConfigFiles + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.UsersConfigFiles != nil { + in, out := &in.UsersConfigFiles, &out.UsersConfigFiles + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigFileRuntime. +func (in *OperatorConfigFileRuntime) DeepCopy() *OperatorConfigFileRuntime { + if in == nil { + return nil + } + out := new(OperatorConfigFileRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigKeeper) DeepCopyInto(out *OperatorConfigKeeper) { + *out = *in + in.Config.DeepCopyInto(&out.Config) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigKeeper. +func (in *OperatorConfigKeeper) DeepCopy() *OperatorConfigKeeper { + if in == nil { + return nil + } + out := new(OperatorConfigKeeper) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigLabel) DeepCopyInto(out *OperatorConfigLabel) { + *out = *in + if in.Include != nil { + in, out := &in.Include, &out.Include + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Exclude != nil { + in, out := &in.Exclude, &out.Exclude + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.Runtime = in.Runtime + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigLabel. +func (in *OperatorConfigLabel) DeepCopy() *OperatorConfigLabel { + if in == nil { + return nil + } + out := new(OperatorConfigLabel) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigLabelRuntime) DeepCopyInto(out *OperatorConfigLabelRuntime) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigLabelRuntime. +func (in *OperatorConfigLabelRuntime) DeepCopy() *OperatorConfigLabelRuntime { + if in == nil { + return nil + } + out := new(OperatorConfigLabelRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigMetrics) DeepCopyInto(out *OperatorConfigMetrics) { + *out = *in + in.Labels.DeepCopyInto(&out.Labels) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigMetrics. +func (in *OperatorConfigMetrics) DeepCopy() *OperatorConfigMetrics { + if in == nil { + return nil + } + out := new(OperatorConfigMetrics) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigMetricsLabels) DeepCopyInto(out *OperatorConfigMetricsLabels) { + *out = *in + if in.Exclude != nil { + in, out := &in.Exclude, &out.Exclude + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigMetricsLabels. +func (in *OperatorConfigMetricsLabels) DeepCopy() *OperatorConfigMetricsLabels { + if in == nil { + return nil + } + out := new(OperatorConfigMetricsLabels) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigReconcile) DeepCopyInto(out *OperatorConfigReconcile) { + *out = *in + out.Runtime = in.Runtime + out.StatefulSet = in.StatefulSet + in.Host.DeepCopyInto(&out.Host) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigReconcile. +func (in *OperatorConfigReconcile) DeepCopy() *OperatorConfigReconcile { + if in == nil { + return nil + } + out := new(OperatorConfigReconcile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigReconcileRuntime) DeepCopyInto(out *OperatorConfigReconcileRuntime) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigReconcileRuntime. +func (in *OperatorConfigReconcileRuntime) DeepCopy() *OperatorConfigReconcileRuntime { + if in == nil { + return nil + } + out := new(OperatorConfigReconcileRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigRestartPolicy) DeepCopyInto(out *OperatorConfigRestartPolicy) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]OperatorConfigRestartPolicyRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigRestartPolicy. +func (in *OperatorConfigRestartPolicy) DeepCopy() *OperatorConfigRestartPolicy { + if in == nil { + return nil + } + out := new(OperatorConfigRestartPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigRestartPolicyRule) DeepCopyInto(out *OperatorConfigRestartPolicyRule) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]OperatorConfigRestartPolicyRuleSet, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make(OperatorConfigRestartPolicyRuleSet, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigRestartPolicyRule. +func (in *OperatorConfigRestartPolicyRule) DeepCopy() *OperatorConfigRestartPolicyRule { + if in == nil { + return nil + } + out := new(OperatorConfigRestartPolicyRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in OperatorConfigRestartPolicyRuleSet) DeepCopyInto(out *OperatorConfigRestartPolicyRuleSet) { + { + in := &in + *out = make(OperatorConfigRestartPolicyRuleSet, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigRestartPolicyRuleSet. +func (in OperatorConfigRestartPolicyRuleSet) DeepCopy() OperatorConfigRestartPolicyRuleSet { + if in == nil { + return nil + } + out := new(OperatorConfigRestartPolicyRuleSet) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigRuntime) DeepCopyInto(out *OperatorConfigRuntime) { + *out = *in + if in.ConfigCRSources != nil { + in, out := &in.ConfigCRSources, &out.ConfigCRSources + *out = make([]ConfigCRSource, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigRuntime. +func (in *OperatorConfigRuntime) DeepCopy() *OperatorConfigRuntime { + if in == nil { + return nil + } + out := new(OperatorConfigRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigStatus) DeepCopyInto(out *OperatorConfigStatus) { + *out = *in + in.Fields.DeepCopyInto(&out.Fields) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigStatus. +func (in *OperatorConfigStatus) DeepCopy() *OperatorConfigStatus { + if in == nil { + return nil + } + out := new(OperatorConfigStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigStatusFields) DeepCopyInto(out *OperatorConfigStatusFields) { + *out = *in + if in.Action != nil { + in, out := &in.Action, &out.Action + *out = new(types.StringBool) + **out = **in + } + if in.Actions != nil { + in, out := &in.Actions, &out.Actions + *out = new(types.StringBool) + **out = **in + } + if in.Error != nil { + in, out := &in.Error, &out.Error + *out = new(types.StringBool) + **out = **in + } + if in.Errors != nil { + in, out := &in.Errors, &out.Errors + *out = new(types.StringBool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigStatusFields. +func (in *OperatorConfigStatusFields) DeepCopy() *OperatorConfigStatusFields { + if in == nil { + return nil + } + out := new(OperatorConfigStatusFields) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigTemplate) DeepCopyInto(out *OperatorConfigTemplate) { + // *out = *in + in.CHI.DeepCopyInto(&out.CHI) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigTemplate. +func (in *OperatorConfigTemplate) DeepCopy() *OperatorConfigTemplate { + if in == nil { + return nil + } + out := new(OperatorConfigTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigUser) DeepCopyInto(out *OperatorConfigUser) { + *out = *in + in.Default.DeepCopyInto(&out.Default) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigUser. +func (in *OperatorConfigUser) DeepCopy() *OperatorConfigUser { + if in == nil { + return nil + } + out := new(OperatorConfigUser) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigWatch) DeepCopyInto(out *OperatorConfigWatch) { + *out = *in + in.Namespaces.DeepCopyInto(&out.Namespaces) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigWatch. +func (in *OperatorConfigWatch) DeepCopy() *OperatorConfigWatch { + if in == nil { + return nil + } + out := new(OperatorConfigWatch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatorConfigWatchNamespaces) DeepCopyInto(out *OperatorConfigWatchNamespaces) { + *out = *in + if in.Include != nil { + in, out := &in.Include, &out.Include + *out = new(types.Strings) + if **in != nil { + in, out := *in, *out + *out = make([]string, len(*in)) + copy(*out, *in) + } + } + if in.Exclude != nil { + in, out := &in.Exclude, &out.Exclude + *out = new(types.Strings) + if **in != nil { + in, out := *in, *out + *out = make([]string, len(*in)) + copy(*out, *in) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigWatchNamespaces. +func (in *OperatorConfigWatchNamespaces) DeepCopy() *OperatorConfigWatchNamespaces { + if in == nil { + return nil + } + out := new(OperatorConfigWatchNamespaces) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodDistribution) DeepCopyInto(out *PodDistribution) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodDistribution. +func (in *PodDistribution) DeepCopy() *PodDistribution { + if in == nil { + return nil + } + out := new(PodDistribution) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodTemplate) DeepCopyInto(out *PodTemplate) { + *out = *in + in.Zone.DeepCopyInto(&out.Zone) + if in.PodDistribution != nil { + in, out := &in.PodDistribution, &out.PodDistribution + *out = make([]PodDistribution, len(*in)) + copy(*out, *in) + } + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodTemplate. +func (in *PodTemplate) DeepCopy() *PodTemplate { + if in == nil { + return nil + } + out := new(PodTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodTemplateZone) DeepCopyInto(out *PodTemplateZone) { + *out = *in + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodTemplateZone. +func (in *PodTemplateZone) DeepCopy() *PodTemplateZone { + if in == nil { + return nil + } + out := new(PodTemplateZone) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodTemplatesIndex) DeepCopyInto(out *PodTemplatesIndex) { + *out = *in + if in.templates != nil { + in, out := &in.templates, &out.templates + *out = make(map[string]*PodTemplate, len(*in)) + for key, val := range *in { + var outVal *PodTemplate + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = new(PodTemplate) + (*in).DeepCopyInto(*out) + } + (*out)[key] = outVal + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodTemplatesIndex. +func (in *PodTemplatesIndex) DeepCopy() *PodTemplatesIndex { + if in == nil { + return nil + } + out := new(PodTemplatesIndex) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PortDistribution) DeepCopyInto(out *PortDistribution) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PortDistribution. +func (in *PortDistribution) DeepCopy() *PortDistribution { + if in == nil { + return nil + } + out := new(PortDistribution) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReconcileHost) DeepCopyInto(out *ReconcileHost) { + *out = *in + in.Wait.DeepCopyInto(&out.Wait) + in.Drop.DeepCopyInto(&out.Drop) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReconcileHost. +func (in *ReconcileHost) DeepCopy() *ReconcileHost { + if in == nil { + return nil + } + out := new(ReconcileHost) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReconcileHostDrop) DeepCopyInto(out *ReconcileHostDrop) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(ReconcileHostDropReplicas) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReconcileHostDrop. +func (in *ReconcileHostDrop) DeepCopy() *ReconcileHostDrop { + if in == nil { + return nil + } + out := new(ReconcileHostDrop) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReconcileHostDropReplicas) DeepCopyInto(out *ReconcileHostDropReplicas) { + *out = *in + if in.OnDelete != nil { + in, out := &in.OnDelete, &out.OnDelete + *out = new(types.StringBool) + **out = **in + } + if in.OnLostVolume != nil { + in, out := &in.OnLostVolume, &out.OnLostVolume + *out = new(types.StringBool) + **out = **in + } + if in.Active != nil { + in, out := &in.Active, &out.Active + *out = new(types.StringBool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReconcileHostDropReplicas. +func (in *ReconcileHostDropReplicas) DeepCopy() *ReconcileHostDropReplicas { + if in == nil { + return nil + } + out := new(ReconcileHostDropReplicas) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReconcileHostWait) DeepCopyInto(out *ReconcileHostWait) { + *out = *in + if in.Exclude != nil { + in, out := &in.Exclude, &out.Exclude + *out = new(types.StringBool) + **out = **in + } + if in.Queries != nil { + in, out := &in.Queries, &out.Queries + *out = new(types.StringBool) + **out = **in + } + if in.Include != nil { + in, out := &in.Include, &out.Include + *out = new(types.StringBool) + **out = **in + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(ReconcileHostWaitReplicas) + (*in).DeepCopyInto(*out) + } + if in.Probes != nil { + in, out := &in.Probes, &out.Probes + *out = new(ReconcileHostWaitProbes) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReconcileHostWait. +func (in *ReconcileHostWait) DeepCopy() *ReconcileHostWait { + if in == nil { + return nil + } + out := new(ReconcileHostWait) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReconcileHostWaitProbes) DeepCopyInto(out *ReconcileHostWaitProbes) { + *out = *in + if in.Startup != nil { + in, out := &in.Startup, &out.Startup + *out = new(types.StringBool) + **out = **in + } + if in.Readiness != nil { + in, out := &in.Readiness, &out.Readiness + *out = new(types.StringBool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReconcileHostWaitProbes. +func (in *ReconcileHostWaitProbes) DeepCopy() *ReconcileHostWaitProbes { + if in == nil { + return nil + } + out := new(ReconcileHostWaitProbes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReconcileHostWaitReplicas) DeepCopyInto(out *ReconcileHostWaitReplicas) { + *out = *in + if in.All != nil { + in, out := &in.All, &out.All + *out = new(types.StringBool) + **out = **in + } + if in.New != nil { + in, out := &in.New, &out.New + *out = new(types.StringBool) + **out = **in + } + if in.Delay != nil { + in, out := &in.Delay, &out.Delay + *out = new(types.Int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReconcileHostWaitReplicas. +func (in *ReconcileHostWaitReplicas) DeepCopy() *ReconcileHostWaitReplicas { + if in == nil { + return nil + } + out := new(ReconcileHostWaitReplicas) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReconcileMacros) DeepCopyInto(out *ReconcileMacros) { + *out = *in + in.Sections.DeepCopyInto(&out.Sections) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReconcileMacros. +func (in *ReconcileMacros) DeepCopy() *ReconcileMacros { + if in == nil { + return nil + } + out := new(ReconcileMacros) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReconcileRuntime) DeepCopyInto(out *ReconcileRuntime) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReconcileRuntime. +func (in *ReconcileRuntime) DeepCopy() *ReconcileRuntime { + if in == nil { + return nil + } + out := new(ReconcileRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SchemaPolicy) DeepCopyInto(out *SchemaPolicy) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SchemaPolicy. +func (in *SchemaPolicy) DeepCopy() *SchemaPolicy { + if in == nil { + return nil + } + out := new(SchemaPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceTemplate) DeepCopyInto(out *ServiceTemplate) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceTemplate. +func (in *ServiceTemplate) DeepCopy() *ServiceTemplate { + if in == nil { + return nil + } + out := new(ServiceTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceTemplatesIndex) DeepCopyInto(out *ServiceTemplatesIndex) { + *out = *in + if in.templates != nil { + in, out := &in.templates, &out.templates + *out = make(map[string]*ServiceTemplate, len(*in)) + for key, val := range *in { + var outVal *ServiceTemplate + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = new(ServiceTemplate) + (*in).DeepCopyInto(*out) + } + (*out)[key] = outVal + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceTemplatesIndex. +func (in *ServiceTemplatesIndex) DeepCopy() *ServiceTemplatesIndex { + if in == nil { + return nil + } + out := new(ServiceTemplatesIndex) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Setting) DeepCopyInto(out *Setting) { + *out = *in + if in.vector != nil { + in, out := &in.vector, &out.vector + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.src != nil { + in, out := &in.src, &out.src + *out = new(SettingSource) + (*in).DeepCopyInto(*out) + } + if in.attributes != nil { + in, out := &in.attributes, &out.attributes + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Setting. +func (in *Setting) DeepCopy() *Setting { + if in == nil { + return nil + } + out := new(Setting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SettingSource) DeepCopyInto(out *SettingSource) { + *out = *in + if in.ValueFrom != nil { + in, out := &in.ValueFrom, &out.ValueFrom + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SettingSource. +func (in *SettingSource) DeepCopy() *SettingSource { + if in == nil { + return nil + } + out := new(SettingSource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Settings) DeepCopyInto(out *Settings) { + *out = *in + if in.data != nil { + in, out := &in.data, &out.data + *out = make(map[string]*Setting, len(*in)) + for key, val := range *in { + var outVal *Setting + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = new(Setting) + (*in).DeepCopyInto(*out) + } + (*out)[key] = outVal + } + } + if in.converter != nil { + out.converter = in.converter.DeepCopySettingsName2KeyConverter() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Settings. +func (in *Settings) DeepCopy() *Settings { + if in == nil { + return nil + } + out := new(Settings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SettingsName2KeyConverterPlain) DeepCopyInto(out *SettingsName2KeyConverterPlain) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SettingsName2KeyConverterPlain. +func (in *SettingsName2KeyConverterPlain) DeepCopy() *SettingsName2KeyConverterPlain { + if in == nil { + return nil + } + out := new(SettingsName2KeyConverterPlain) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SettingsName2KeyConverterPrefix) DeepCopyInto(out *SettingsName2KeyConverterPrefix) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SettingsName2KeyConverterPrefix. +func (in *SettingsName2KeyConverterPrefix) DeepCopy() *SettingsName2KeyConverterPrefix { + if in == nil { + return nil + } + out := new(SettingsName2KeyConverterPrefix) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SettingsNormalizerOptions) DeepCopyInto(out *SettingsNormalizerOptions) { + *out = *in + if in.Replacers != nil { + in, out := &in.Replacers, &out.Replacers + *out = make([]*util.Replacer, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(util.Replacer) + *in = *out + } + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SettingsNormalizerOptions. +func (in *SettingsNormalizerOptions) DeepCopy() *SettingsNormalizerOptions { + if in == nil { + return nil + } + out := new(SettingsNormalizerOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SettingsUser) DeepCopyInto(out *SettingsUser) { + *out = *in + in.Settings.DeepCopyInto(&out.Settings) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SettingsUser. +func (in *SettingsUser) DeepCopy() *SettingsUser { + if in == nil { + return nil + } + out := new(SettingsUser) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Status) DeepCopyInto(out *Status) { + // *out = *in + out.CHOpVersion = in.CHOpVersion + out.CHOpCommit = in.CHOpCommit + out.CHOpDate = in.CHOpDate + out.CHOpIP = in.CHOpIP + out.ClustersCount = in.ClustersCount + out.ShardsCount = in.ShardsCount + out.ReplicasCount = in.ReplicasCount + out.HostsCount = in.HostsCount + out.Status = in.Status + out.TaskID = in.TaskID + out.Action = in.Action + out.Error = in.Error + out.HostsUpdatedCount = in.HostsUpdatedCount + out.HostsAddedCount = in.HostsAddedCount + out.HostsUnchangedCount = in.HostsUnchangedCount + out.HostsFailedCount = in.HostsFailedCount + out.HostsCompletedCount = in.HostsCompletedCount + out.HostsDeletedCount = in.HostsDeletedCount + out.HostsDeleteCount = in.HostsDeleteCount + out.Endpoint = in.Endpoint + if in.TaskIDsStarted != nil { + in, out := &in.TaskIDsStarted, &out.TaskIDsStarted + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TaskIDsCompleted != nil { + in, out := &in.TaskIDsCompleted, &out.TaskIDsCompleted + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Actions != nil { + in, out := &in.Actions, &out.Actions + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Errors != nil { + in, out := &in.Errors, &out.Errors + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Pods != nil { + in, out := &in.Pods, &out.Pods + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PodIPs != nil { + in, out := &in.PodIPs, &out.PodIPs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.FQDNs != nil { + in, out := &in.FQDNs, &out.FQDNs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Endpoints != nil { + in, out := &in.Endpoints, &out.Endpoints + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.NormalizedCR != nil { + in, out := &in.NormalizedCR, &out.NormalizedCR + *out = new(ClickHouseInstallation) + (*in).DeepCopyInto(*out) + } + if in.NormalizedCRCompleted != nil { + in, out := &in.NormalizedCRCompleted, &out.NormalizedCRCompleted + *out = new(ClickHouseInstallation) + (*in).DeepCopyInto(*out) + } + if in.ActionPlan != nil { + in, out := &in.ActionPlan, &out.ActionPlan + *out = new(ActionPlan) + (*in).DeepCopyInto(*out) + } + if in.HostsWithTablesCreated != nil { + in, out := &in.HostsWithTablesCreated, &out.HostsWithTablesCreated + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.HostsWithReplicaCaughtUp != nil { + in, out := &in.HostsWithReplicaCaughtUp, &out.HostsWithReplicaCaughtUp + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.UsedTemplates != nil { + in, out := &in.UsedTemplates, &out.UsedTemplates + *out = make([]*TemplateRef, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(TemplateRef) + **out = **in + } + } + } + // out.mu = in.mu + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. +func (in *Status) DeepCopy() *Status { + if in == nil { + return nil + } + out := new(Status) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StorageManagement) DeepCopyInto(out *StorageManagement) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageManagement. +func (in *StorageManagement) DeepCopy() *StorageManagement { + if in == nil { + return nil + } + out := new(StorageManagement) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in TargetSelector) DeepCopyInto(out *TargetSelector) { + { + in := &in + *out = make(TargetSelector, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSelector. +func (in TargetSelector) DeepCopy() TargetSelector { + if in == nil { + return nil + } + out := new(TargetSelector) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplateRef) DeepCopyInto(out *TemplateRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateRef. +func (in *TemplateRef) DeepCopy() *TemplateRef { + if in == nil { + return nil + } + out := new(TemplateRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Templates) DeepCopyInto(out *Templates) { + *out = *in + if in.HostTemplates != nil { + in, out := &in.HostTemplates, &out.HostTemplates + *out = make([]HostTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.PodTemplates != nil { + in, out := &in.PodTemplates, &out.PodTemplates + *out = make([]PodTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.VolumeClaimTemplates != nil { + in, out := &in.VolumeClaimTemplates, &out.VolumeClaimTemplates + *out = make([]VolumeClaimTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ServiceTemplates != nil { + in, out := &in.ServiceTemplates, &out.ServiceTemplates + *out = make([]ServiceTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.HostTemplatesIndex != nil { + in, out := &in.HostTemplatesIndex, &out.HostTemplatesIndex + *out = new(HostTemplatesIndex) + (*in).DeepCopyInto(*out) + } + if in.PodTemplatesIndex != nil { + in, out := &in.PodTemplatesIndex, &out.PodTemplatesIndex + *out = new(PodTemplatesIndex) + (*in).DeepCopyInto(*out) + } + if in.VolumeClaimTemplatesIndex != nil { + in, out := &in.VolumeClaimTemplatesIndex, &out.VolumeClaimTemplatesIndex + *out = new(VolumeClaimTemplatesIndex) + (*in).DeepCopyInto(*out) + } + if in.ServiceTemplatesIndex != nil { + in, out := &in.ServiceTemplatesIndex, &out.ServiceTemplatesIndex + *out = new(ServiceTemplatesIndex) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Templates. +func (in *Templates) DeepCopy() *Templates { + if in == nil { + return nil + } + out := new(Templates) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplatesList) DeepCopyInto(out *TemplatesList) { + *out = *in + if in.ServiceTemplates != nil { + in, out := &in.ServiceTemplates, &out.ServiceTemplates + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplatesList. +func (in *TemplatesList) DeepCopy() *TemplatesList { + if in == nil { + return nil + } + out := new(TemplatesList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VolumeClaimTemplate) DeepCopyInto(out *VolumeClaimTemplate) { + *out = *in + out.StorageManagement = in.StorageManagement + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeClaimTemplate. +func (in *VolumeClaimTemplate) DeepCopy() *VolumeClaimTemplate { + if in == nil { + return nil + } + out := new(VolumeClaimTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VolumeClaimTemplatesIndex) DeepCopyInto(out *VolumeClaimTemplatesIndex) { + *out = *in + if in.templates != nil { + in, out := &in.templates, &out.templates + *out = make(map[string]*VolumeClaimTemplate, len(*in)) + for key, val := range *in { + var outVal *VolumeClaimTemplate + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = new(VolumeClaimTemplate) + (*in).DeepCopyInto(*out) + } + (*out)[key] = outVal + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeClaimTemplatesIndex. +func (in *VolumeClaimTemplatesIndex) DeepCopy() *VolumeClaimTemplatesIndex { + if in == nil { + return nil + } + out := new(VolumeClaimTemplatesIndex) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ZookeeperConfig) DeepCopyInto(out *ZookeeperConfig) { + *out = *in + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = make(ZookeeperNodes, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.UseCompression != nil { + in, out := &in.UseCompression, &out.UseCompression + *out = new(types.StringBool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZookeeperConfig. +func (in *ZookeeperConfig) DeepCopy() *ZookeeperConfig { + if in == nil { + return nil + } + out := new(ZookeeperConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ZookeeperNode) DeepCopyInto(out *ZookeeperNode) { + *out = *in + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(types.Int32) + **out = **in + } + if in.Secure != nil { + in, out := &in.Secure, &out.Secure + *out = new(types.StringBool) + **out = **in + } + if in.AvailabilityZone != nil { + in, out := &in.AvailabilityZone, &out.AvailabilityZone + *out = new(types.String) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZookeeperNode. +func (in *ZookeeperNode) DeepCopy() *ZookeeperNode { + if in == nil { + return nil + } + out := new(ZookeeperNode) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ZookeeperNodes) DeepCopyInto(out *ZookeeperNodes) { + { + in := &in + *out = make(ZookeeperNodes, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZookeeperNodes. +func (in ZookeeperNodes) DeepCopy() ZookeeperNodes { + if in == nil { + return nil + } + out := new(ZookeeperNodes) + in.DeepCopyInto(out) + return *out +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/copy_cr_options.go b/pkg/vendored/altinity-clickhouse/common/types/copy_cr_options.go new file mode 100644 index 00000000..5e1a3051 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/copy_cr_options.go @@ -0,0 +1,23 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +// CopyCROptions specifies options for custom resource copier +type CopyCROptions struct { + // SkipStatus specifies whether to copy status + SkipStatus bool + // SkipManagedFields specifies whether to copy managed fields + SkipManagedFields bool +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/data_source.go b/pkg/vendored/altinity-clickhouse/common/types/data_source.go new file mode 100644 index 00000000..5bc4f377 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/data_source.go @@ -0,0 +1,44 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + core "k8s.io/api/core/v1" +) + +// DataSource is a set of possible data sources +type DataSource struct { + // SecretKeyRef points to a secret and mirrors k8s SecretSource type + SecretKeyRef *core.SecretKeySelector `json:"secretKeyRef,omitempty" yaml:"secretKeyRef,omitempty"` +} + +func (in *DataSource) DeepCopy() *DataSource { + if in == nil { + return nil + } + out := new(DataSource) + in.DeepCopyInto(out) + return out +} + +func (in *DataSource) DeepCopyInto(out *DataSource) { + *out = *in + if in.SecretKeyRef != nil { + in, out := &in.SecretKeyRef, &out.SecretKeyRef + *out = new(core.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + return +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/host_scope_address.go b/pkg/vendored/altinity-clickhouse/common/types/host_scope_address.go new file mode 100644 index 00000000..7703030a --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/host_scope_address.go @@ -0,0 +1,132 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +// CycleSpec defines spec of a cycle, such as size +type CycleSpec struct { + // Size specifies size of a cycle + Size int +} + +// NewCycleSpec creates new CycleSpec +func NewCycleSpec(size int) *CycleSpec { + return &CycleSpec{ + Size: size, + } +} + +// IsValid specifies whether spec is a valid one +func (s *CycleSpec) IsValid() bool { + if s == nil { + return false + } + + return s.Size > 0 +} + +// CycleAddress defines cycle address of an entity +type CycleAddress struct { + // CycleIndex specifies index of the cycle within something bigger + CycleIndex int + // Index specifies index within the cycle + Index int +} + +// NewCycleAddress creates new CycleAddress +func NewCycleAddress() *CycleAddress { + return &CycleAddress{} +} + +// Init initializes the CycleAddress +func (s *CycleAddress) Init() { + if s == nil { + return + } + s.CycleIndex = 0 + s.Index = 0 +} + +// Inc increases the CycleAddress +func (s *CycleAddress) Inc(spec *CycleSpec) { + if s == nil { + return + } + // Shift index within the cycle + s.Index++ + // In case of overflow - shift to next cycle + if spec.IsValid() && (s.Index >= spec.Size) { + s.CycleIndex++ + s.Index = 0 + } +} + +// ScopeAddress defines scope address of an entity +type ScopeAddress struct { + // CycleSpec specifies cycle which to be used to specify CycleAddress + CycleSpec *CycleSpec + // CycleAddress specifies CycleAddress within the scope + CycleAddress *CycleAddress + // Index specifies index within the scope + Index int +} + +// NewScopeAddress creates new ScopeAddress +func NewScopeAddress(cycleSize int) *ScopeAddress { + return &ScopeAddress{ + CycleSpec: NewCycleSpec(cycleSize), + CycleAddress: NewCycleAddress(), + } +} + +// Init initializes the ScopeAddress +func (s *ScopeAddress) Init() { + if s == nil { + return + } + s.CycleAddress.Init() + s.Index = 0 +} + +// Inc increases the ScopeAddress +func (s *ScopeAddress) Inc() { + if s == nil { + return + } + s.CycleAddress.Inc(s.CycleSpec) + s.Index++ +} + +// HostScopeAddress specifies address of a host +type HostScopeAddress struct { + // CRScopeAddress specifies address of a host within CHI scope + CRScopeAddress *ScopeAddress + // ClusterScopeAddress specifies address of a host within cluster scope + ClusterScopeAddress *ScopeAddress + // ClusterIndex specifies index of a cluster within CHI + ClusterIndex int + // ShardIndex specifies index of a shard within cluster + ShardIndex int + // ReplicaIndex specifies index of a replica within cluster + ReplicaIndex int +} + +// NewHostScopeAddress creates new HostScopeAddress +func NewHostScopeAddress(crScopeCycleSize, clusterScopeCycleSize int) (a *HostScopeAddress) { + a = &HostScopeAddress{ + CRScopeAddress: NewScopeAddress(crScopeCycleSize), + ClusterScopeAddress: NewScopeAddress(clusterScopeCycleSize), + } + return a +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/id.go b/pkg/vendored/altinity-clickhouse/common/types/id.go new file mode 100644 index 00000000..4b19a3ec --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/id.go @@ -0,0 +1,60 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "strings" + + "github.com/google/uuid" +) + +// Id defines id representation with possibility to be optional +type Id String + +const autoPrefix = "auto-" + +// NewId creates new variable +func NewId() *Id { + return (*Id)(NewString(uuid.New().String())) +} + +func NewAutoId() *Id { + return (*Id)(NewString(autoPrefix + uuid.New().String())) +} + +// HasValue checks whether value is specified +func (id *Id) HasValue() bool { + return len(id.Value()) > 0 +} + +// Value returns value +func (id *Id) Value() string { + return (*String)(id).Value() +} + +// String casts to a string +func (id *Id) String() string { + return id.Value() +} + +// IsAutoId checks whether id is an auto-id +func (id *Id) IsAutoId() bool { + return strings.HasPrefix(id.Value(), autoPrefix) +} + +// MergeFrom merges value from another variable +func (id *Id) MergeFrom(from *Id) *Id { + return (*Id)((*String)(id).MergeFrom((*String)(from))) +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/int32.go b/pkg/vendored/altinity-clickhouse/common/types/int32.go new file mode 100644 index 00000000..73d9d518 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/int32.go @@ -0,0 +1,118 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import "strconv" + +// Int32 defines int32 representation with possibility to be optional +type Int32 int32 + +// NewInt32 creates new variable +func NewInt32(i int32) *Int32 { + i32 := new(Int32) + *i32 = Int32(i) + return i32 +} + +// From casts int32 +func (i *Int32) From(value int32) *Int32 { + return NewInt32(value) +} + +// String casts to a string +func (i *Int32) String() string { + if i == nil { + return "" + } + return strconv.Itoa(i.IntValue()) +} + +// HasValue checks whether value is specified +func (i *Int32) HasValue() bool { + return i != nil +} + +// Value returns value +func (i *Int32) Value() int32 { + if i == nil { + return 0 + } + + return int32(*i) +} + +// IntValue returns int value +func (i *Int32) IntValue() int { + if i == nil { + return 0 + } + + return int(*i) +} + +// IsValid checks whether var has a proper value +func (i *Int32) IsValid() bool { + return i.HasValue() +} + +// Normalize normalizes value with fallback to defaultValue in case initial value is incorrect +func (i *Int32) Normalize(defaultValue int32) *Int32 { + if i.IsValid() { + return i + } + + // Value is unrecognized, return default value + return NewInt32(defaultValue) +} + +// MergeFrom merges value from another variable +func (i *Int32) MergeFrom(from *Int32) *Int32 { + if from == nil { + // Nothing to merge from, keep original value + return i + } + + // From now on we have `from` specified + + if i == nil { + // Recipient is not specified, just use `from` value + return from + } + + // Both recipient and `from` are specified, need to pick one value. + // Prefer local value + return i +} + +// Equal checks whether is equal to another +func (i *Int32) Equal(to *Int32) bool { + if (i == nil) && (to == nil) { + // Consider nil equal + return true + } + + return i.EqualValue(to) +} + +// EqualValue checks whether has equal values +func (i *Int32) EqualValue(to *Int32) bool { + if !i.HasValue() || !to.HasValue() { + // Need to compare values only + return false + } + + // Both have value available, comparable + return i.Value() == to.Value() +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/list.go b/pkg/vendored/altinity-clickhouse/common/types/list.go new file mode 100644 index 00000000..b8770626 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/list.go @@ -0,0 +1,21 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +type List map[string]string + +func (l List) Get(name string) string { + return l[name] +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/matchable.go b/pkg/vendored/altinity-clickhouse/common/types/matchable.go new file mode 100644 index 00000000..e7d24a22 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/matchable.go @@ -0,0 +1,35 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "regexp" + "strings" +) + +// Matchable defines string that can be matched against another string +type Matchable string + +// Match checks whether provided string matches this matchable +func (m *Matchable) Match(str string) bool { + if m == nil { + return false + } + // Build regex by replacing all * with .* and explicitly specifying start and end of the string + regex := "^" + strings.Replace(string(*m), "*", ".*", -1) + "$" + // Check whether string matches regexp + matches, _ := regexp.MatchString(regex, str) + return matches +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/object_address.go b/pkg/vendored/altinity-clickhouse/common/types/object_address.go new file mode 100644 index 00000000..f5da8e6f --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/object_address.go @@ -0,0 +1,33 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +type ObjectAddress struct { + Namespace string + Name string + Key string +} + +func (a ObjectAddress) AnyEmpty() bool { + return (a.Namespace == "") || (a.Name == "") || (a.Key == "") +} + +func (a ObjectAddress) String() string { + return a.Render("/") +} + +func (a ObjectAddress) Render(sep string) string { + return a.Namespace + sep + a.Name + sep + a.Key +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/object_status.go b/pkg/vendored/altinity-clickhouse/common/types/object_status.go new file mode 100644 index 00000000..39cc2730 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/object_status.go @@ -0,0 +1,37 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +// ObjectStatus specifies object status +type ObjectStatus string + +// Possible values for object status +const ( + ObjectStatusRequested ObjectStatus = "requested" + ObjectStatusModified ObjectStatus = "modified" + ObjectStatusSame ObjectStatus = "same" + + ObjectStatusFound ObjectStatus = "found" + ObjectStatusCreated ObjectStatus = "created" + ObjectStatusUnknown ObjectStatus = "unknown" +) + +func (s ObjectStatus) Is(b ObjectStatus) bool { + return s == b +} + +func (s ObjectStatus) String() string { + return string(s) +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/port.go b/pkg/vendored/altinity-clickhouse/common/types/port.go new file mode 100644 index 00000000..655b9fb0 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/port.go @@ -0,0 +1,64 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +const ( + // PortMayBeAssignedLaterOrLeftUnused value means that port + // is not assigned yet and is expected to be assigned later. + PortMayBeAssignedLaterOrLeftUnused = int32(0) +) + +// PortUnassigned returns value for unassigned port +func PortUnassigned() int32 { + return PortMayBeAssignedLaterOrLeftUnused +} + +// IsPortAssigned checks whether port is assigned +func IsPortAssigned(port int32) bool { + return port != PortUnassigned() +} + +// IsPortUnassigned checks whether port is unassigned +func IsPortUnassigned(port int32) bool { + return port == PortUnassigned() +} + +// IsPortInvalid checks whether port is invalid +func IsPortInvalid(port int32) bool { + return (port <= 0) || (port >= 65535) +} + +// EnsurePortValue ensures port either: +// - already has own value assigned +// - or has provided value +// - or value is fell back to default +func EnsurePortValue(port, value, _default *Int32) *Int32 { + // Port may already be explicitly specified in podTemplate or by portDistribution + if port.HasValue() { + // Port has a value already + return port + } + + // Port has no explicitly assigned value + + // Let's use provided value real value + if value.HasValue() { + // Provided value is a real value, use it + return value + } + + // Fallback to default value + return _default +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/reconcile_attributes.go b/pkg/vendored/altinity-clickhouse/common/types/reconcile_attributes.go new file mode 100644 index 00000000..68b533df --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/reconcile_attributes.go @@ -0,0 +1,219 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "fmt" +) + +const ( + TagExclude Tag = "exclude" + TagLowPriority Tag = "low_priority" +) + +// ReconcileAttributes defines reconcile status and attributes +type ReconcileAttributes struct { + status ObjectStatus + tags Tags +} + +func (in *ReconcileAttributes) DeepCopy() *ReconcileAttributes { + if in == nil { + return nil + } + out := new(ReconcileAttributes) + in.DeepCopyInto(out) + return out +} + +func (in *ReconcileAttributes) DeepCopyInto(out *ReconcileAttributes) { + *out = *in + if in.tags != nil { + in, out := &in.tags, &out.tags + *out = make(Tags, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// NewReconcileAttributes creates new reconcile attributes +func NewReconcileAttributes() *ReconcileAttributes { + return &ReconcileAttributes{ + status: ObjectStatusUnknown, + } +} + +// HasIntersectionWith checks whether attributes `a` has intersection with `b` +func (a *ReconcileAttributes) HasIntersectionWith(b *ReconcileAttributes) bool { + if a == nil { + return false + } + if b == nil { + return false + } + switch { + case a.GetStatus().Is(b.GetStatus()): + return true + case a.tags.HasIntersectionWith(b.tags): + return true + } + return false +} + +// SetStatus sets object status +func (a *ReconcileAttributes) SetStatus(status ObjectStatus) *ReconcileAttributes { + if a == nil { + return a + } + a.status = status + return a +} + +// GetStatus gets object status +func (a *ReconcileAttributes) GetStatus() ObjectStatus { + if a == nil { + return ObjectStatusUnknown + } + return a.status +} + +// SetExclude sets 'exclude' attribute +func (a *ReconcileAttributes) SetExclude() *ReconcileAttributes { + if a == nil { + return a + } + a.tags.Set(TagExclude) + return a +} + +// UnsetExclude unsets 'exclude' attribute +func (a *ReconcileAttributes) UnsetExclude() *ReconcileAttributes { + if a == nil { + return a + } + a.tags.UnSet(TagExclude) + return a +} + +// IsExclude checks whether 'exclude' attribute is set +func (a *ReconcileAttributes) IsExclude() bool { + if a == nil { + return false + } + return a.tags.Has(TagExclude) +} + +// SetLowPriority sets 'LowPriority' attribute +func (a *ReconcileAttributes) SetLowPriority() *ReconcileAttributes { + if a == nil { + return a + } + a.tags.Set(TagLowPriority) + return a +} + +// UnsetLowPriority unsets 'LowPriority' attribute +func (a *ReconcileAttributes) UnsetLowPriority() *ReconcileAttributes { + if a == nil { + return a + } + a.tags.UnSet(TagLowPriority) + return a +} + +// IsLowPriority checks whether 'LowPriority' attribute is set +func (a *ReconcileAttributes) IsLowPriority() bool { + if a == nil { + return false + } + return a.tags.Has(TagLowPriority) +} + +// String returns string form +func (a *ReconcileAttributes) String() string { + if a == nil { + return "(nil)" + } + + return fmt.Sprintf("status: %s, tags: %v", a.status, a.tags) +} + +// ReconcileAttributesCounters defines reconcile status and attributes counters +type ReconcileAttributesCounters struct { + status map[ObjectStatus]int + total int + counters int +} + +// NewReconcileAttributesCounters creates new reconcile attributes counters +func NewReconcileAttributesCounters() *ReconcileAttributesCounters { + return &ReconcileAttributesCounters{ + status: make(map[ObjectStatus]int), + } +} + +// Add adds to counters provided ReconcileAttributes +func (c *ReconcileAttributesCounters) Add(a *ReconcileAttributes) { + if c == nil { + return + } + + value, ok := c.status[a.GetStatus()] + if ok { + value++ + } else { + value = 1 + } + + c.status[a.GetStatus()] = value + c.total++ + c.counters = len(c.status) +} + +// HasOnly checks whether counters have specified status items only +func (c *ReconcileAttributesCounters) HasOnly(status ObjectStatus) bool { + return c.getCounterByStatus(status) == c.total +} + +// HasDrift checks whether any host has a status other than ObjectStatusSame, +// meaning child resources have diverged from the desired state. +func (c *ReconcileAttributesCounters) HasDrift() bool { + return !c.HasOnly(ObjectStatusSame) +} + +func (c *ReconcileAttributesCounters) String() string { + if c == nil { + return "" + } + + res := "" + for k, v := range c.status { + res += fmt.Sprintf("%s: %d ", k, v) + } + return res +} + +// getCounterByStatus +func (c *ReconcileAttributesCounters) getCounterByStatus(status ObjectStatus) int { + if c == nil { + return 0 + } + if num, ok := c.status[status]; ok { + return num + } + return 0 +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/status_options.go b/pkg/vendored/altinity-clickhouse/common/types/status_options.go new file mode 100644 index 00000000..ded50742 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/status_options.go @@ -0,0 +1,76 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +// CopyStatusOptions specifies what parts to copy in status +type CopyStatusOptions struct { + CopyStatusFieldGroup + CopyStatusField +} + +type CopyStatusFieldGroup struct { + FieldGroupActions bool + FieldGroupErrors bool + FieldGroupNormalized bool + FieldGroupMain bool + FieldGroupWholeStatus bool + FieldGroupInheritable bool +} + +type CopyStatusField struct { + Copy Status + Merge Status +} + +// UpdateStatusOptions defines how to update CHI status +type UpdateStatusOptions struct { + CopyStatusOptions + TolerateAbsence bool +} + +type Status struct { + CHOpVersion bool + CHOpCommit bool + CHOpDate bool + CHOpIP bool + ClustersCount bool + ShardsCount bool + ReplicasCount bool + HostsCount bool + Status bool + TaskID bool + TaskIDsStarted bool + TaskIDsCompleted bool + Action bool + Actions bool + Error bool + Errors bool + HostsUpdatedCount bool + HostsAddedCount bool + HostsUnchangedCount bool + HostsFailedCount bool + HostsCompletedCount bool + HostsDeletedCount bool + HostsDeleteCount bool + Pods bool + PodIPs bool + FQDNs bool + Endpoint bool + NormalizedCR bool + NormalizedCRCompleted bool + ActionPlan bool + HostsWithTablesCreated bool + UsedTemplates bool +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/string.go b/pkg/vendored/altinity-clickhouse/common/types/string.go new file mode 100644 index 00000000..48d3ed0e --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/string.go @@ -0,0 +1,91 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +// String defines string representation with possibility to be optional +type String string + +// NewString creates new variable +func NewString(str string) *String { + s := new(String) + *s = String(str) + return s +} + +// From casts string +func (s *String) From(value string) *String { + return NewString(value) +} + +// HasValue checks whether value is specified +func (s *String) HasValue() bool { + return s != nil +} + +// Value returns value +func (s *String) Value() string { + if s == nil { + return "" + } + + return string(*s) +} + +// String casts to a string +func (s *String) String() string { + return s.Value() +} + +// IsValid checks whether var has a proper value +func (s *String) IsValid() bool { + return s.HasValue() +} + +// Len calculates len of the string +func (s *String) Len() int { + if s == nil { + return 0 + } + return len(s.String()) +} + +// Normalize normalizes value with fallback to defaultValue in case initial value is incorrect +func (s *String) Normalize(defaultValue string) *String { + if s.IsValid() { + return s + } + + // Value is unrecognized, return default value + return NewString(defaultValue) +} + +// MergeFrom merges value from another variable +func (s *String) MergeFrom(from *String) *String { + if from == nil { + // Nothing to merge from, keep original value + return s + } + + // From now on we have `from` specified + + if s == nil { + // Recipient is not specified, just use `from` value + return from + } + + // Both recipient and `from` are specified, need to pick one value. + // Prefer local value + return s +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/string_bool.go b/pkg/vendored/altinity-clickhouse/common/types/string_bool.go new file mode 100644 index 00000000..7ede221e --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/string_bool.go @@ -0,0 +1,230 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import "strings" + +// StringBool defines string representation of a bool type +type StringBool string + +// Set of string boolean constants +const ( + StringBool0 = "0" + StringBool1 = "1" + StringBoolFalseFirstCapital = "False" + StringBoolFalseLowercase = "false" + StringBoolTrueFirstCapital = "True" + StringBoolTrueLowercase = "true" + StringBoolNoFirstCapital = "No" + StringBoolNoLowercase = "no" + StringBoolYesFirstCapital = "Yes" + StringBoolYesLowercase = "yes" + StringBoolOffFirstCapital = "Off" + StringBoolOffLowercase = "off" + StringBoolOnFirstCapital = "On" + StringBoolOnLowercase = "on" + StringBoolDisableFirstCapital = "Disable" + StringBoolDisableLowercase = "disable" + StringBoolEnableFirstCapital = "Enable" + StringBoolEnableLowercase = "enable" + StringBoolDisabledFirstCapital = "Disabled" + StringBoolDisabledLowercase = "disabled" + StringBoolEnabledFirstCapital = "Enabled" + StringBoolEnabledLowercase = "enabled" +) + +// NewStringBool creates new StringBool variable with optional value +func NewStringBool(value ...bool) *StringBool { + r := new(StringBool) + if len(value) > 0 { + if value[0] { + *r = StringBoolTrueFirstCapital + return r + } + } + *r = StringBoolFalseFirstCapital + return r +} + +// From casts bool to a StringBool +func (s *StringBool) From(value bool) *StringBool { + return NewStringBool(value) +} + +// String casts StringBool to a string +func (s *StringBool) String() string { + if s == nil { + return "" + } + return string(*s) +} + +// HasValue checks whether value is specified +func (s *StringBool) HasValue() bool { + return s != nil +} + +// Value returns bool value +func (s *StringBool) Value() bool { + if s == nil { + return false + } + + if s.IsTrue() { + return true + } + + if s.IsFalse() { + return false + } + + // Default + return false +} + +// IsValid checks whether StringBool has a proper value +func (s *StringBool) IsValid() bool { + switch strings.ToLower(s.String()) { + case + StringBool0, + StringBool1, + + StringBoolFalseLowercase, + StringBoolTrueLowercase, + + StringBoolNoLowercase, + StringBoolYesLowercase, + + StringBoolOffLowercase, + StringBoolOnLowercase, + + StringBoolDisableLowercase, + StringBoolEnableLowercase, + + StringBoolDisabledLowercase, + StringBoolEnabledLowercase: + return true + + default: + return false + } +} + +// IsFalse checks whether str is a string as bool "false" value +func (s *StringBool) IsFalse() bool { + switch strings.ToLower(s.String()) { + case + StringBool0, + StringBoolFalseLowercase, + StringBoolNoLowercase, + StringBoolOffLowercase, + StringBoolDisableLowercase, + StringBoolDisabledLowercase: + return true + + default: + return false + } +} + +// IsTrue checks whether str is a string as bool "true" value +func (s *StringBool) IsTrue() bool { + switch strings.ToLower(s.String()) { + case + StringBool1, + StringBoolTrueLowercase, + StringBoolYesLowercase, + StringBoolOnLowercase, + StringBoolEnableLowercase, + StringBoolEnabledLowercase: + return true + + default: + return false + } +} + +// CastTo01 casts string-bool into string "0"/"1" +func (s *StringBool) CastTo01(defaultValue bool) string { + // True and False string values + _true := StringBool1 + _false := StringBool0 + + if s.IsTrue() { + return _true + } + if s.IsFalse() { + return _false + } + + // String value unrecognized, return default value + + if defaultValue { + return _true + } + + return _false +} + +// CastToStringTrueFalse casts string-bool into string "true"/"false" +func (s *StringBool) CastToStringTrueFalse(defaultValue bool) string { + // True and False values + _true := StringBoolTrueLowercase + _false := StringBoolFalseLowercase + + if s.IsTrue() { + return _true + } + if s.IsFalse() { + return _false + } + + // String value unrecognized, return default value + + if defaultValue { + return _true + } + + return _false +} + +// Normalize normalizes StringBool value with fallback to defaultValue in case initial value is incorrect +func (s *StringBool) Normalize(defaultValue bool) *StringBool { + if s.IsValid() { + return s + } + + // Value is unrecognized, return default value + return NewStringBool(defaultValue) +} + +// MergeFrom merges value from another variable +func (s *StringBool) MergeFrom(from *StringBool) *StringBool { + if from == nil { + // Nothing to merge from, keep original value + return s + } + + // From now on we have `from` specified + + if s == nil { + // Recipient is not specified, just use `from` value + return from + } + + // Both recipient and `from` are specified, need to pick one value. + // Prefer local value + return s +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/strings.go b/pkg/vendored/altinity-clickhouse/common/types/strings.go new file mode 100644 index 00000000..dd7931b6 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/strings.go @@ -0,0 +1,124 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "strings" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" +) + +// Strings defines set of strings representation with possibility to be optional +type Strings []string + +// NewStrings creates new variable +func NewStrings(str []string) *Strings { + s := new(Strings) + *s = append(*s, str...) + *s = util.Unique(*s) + return s +} + +// From casts string +func (s *Strings) From(value []string) *Strings { + return NewStrings(value) +} + +// HasValue checks whether value is specified +func (s *Strings) HasValue() bool { + return s.IsValid() && (s.Len() > 0) +} + +func (s *Strings) Has(needle string) bool { + if !s.HasValue() { + return false + } + return util.InArray(needle, s.Value()) +} + +func (s *Strings) Match(needle string) bool { + if !s.HasValue() { + return false + } + return util.MatchArrayOfRegexps(needle, s.Value()) +} + +// Value returns value +func (s *Strings) Value() []string { + if s.IsValid() { + return *s + } + return nil +} + +func (s *Strings) First() string { + if s.HasValue() { + return (*s)[0] + } + return "" +} + +// String casts to a string +func (s *Strings) String() string { + return strings.Join(s.Value(), " ") +} + +// IsValid checks whether var has a proper value +func (s *Strings) IsValid() bool { + return s != nil +} + +// Len calculates len of the string +func (s *Strings) Len() int { + if s.IsValid() { + return len(*s) + } + return 0 +} + +// Normalize normalizes value with fallback to defaultValue in case initial value is incorrect +func (s *Strings) Normalize(defaultValue []string) *Strings { + if s.IsValid() { + return s + } + + // Value is unrecognized, return default value + return NewStrings(defaultValue) +} + +// MergeFrom merges value from another variable +func (s *Strings) MergeFrom(from *Strings) *Strings { + if from == nil { + // Nothing to merge from, keep original value + return s + } + + // From now on we have `from` specified + + if s == nil { + // Recipient is not specified, just use `from` value + return from + } + + // Both recipient and `from` are specified, need to merge + + *s = append(*s, from.Value()...) + + return NewStrings(s.Value()) +} + +func (s *Strings) Append(str string) *Strings { + return s.MergeFrom(NewStrings([]string{str})) +} diff --git a/pkg/vendored/altinity-clickhouse/common/types/tags.go b/pkg/vendored/altinity-clickhouse/common/types/tags.go new file mode 100644 index 00000000..575cc932 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/common/types/tags.go @@ -0,0 +1,100 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "encoding/json" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" +) + +type Tag string +type Tags map[Tag]int + +// NewTags creates new variable +func NewTags() map[Tag]int { + return make(map[Tag]int) +} + +func (t Tags) Set(tag Tag) Tags { + if t == nil { + return nil + } + t[tag] = 1 + + return t +} + +func (t Tags) SetValue(tag Tag, value int) Tags { + if t == nil { + return nil + } + t[tag] = value + + return t +} + +func (t Tags) Add(b Tags) Tags { + if len(b) == 0 { + // Nothing to add + return t + } + + for keyB, valueB := range b { + if t.Has(keyB) { + t.SetValue(keyB, t.Get(keyB)+valueB) + } else { + t.SetValue(keyB, valueB) + } + } + + return t +} + +func (t Tags) UnSet(tag Tag) Tags { + util.MapDeleteKeys(t, tag) + return t +} + +func (t Tags) Get(tag Tag) int { + if t == nil { + return 0 + } + if value, ok := t[tag]; ok { + return value + } + return 0 +} + +func (t Tags) Has(tag Tag) bool { + return util.MapHasKeys(t, tag) +} + +func (t Tags) Equal(b Tags) bool { + return util.MapsAreTheSame(t, b) +} + +func (t Tags) HasIntersectionWith(b Tags) bool { + return util.MapsHaveKeyValuePairsIntersection(t, b) +} + +// String casts to a string +func (t Tags) String() string { + if t == nil { + return "" + } + b, _ := json.Marshal(t) + return string(b) +} diff --git a/pkg/vendored/altinity-clickhouse/crds/clickhouse-operator-install-bundle.yaml b/pkg/vendored/altinity-clickhouse/crds/clickhouse-operator-install-bundle.yaml new file mode 100644 index 00000000..743e3bc0 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/crds/clickhouse-operator-install-bundle.yaml @@ -0,0 +1,6256 @@ +# Template Parameters: +# +# KIND=ClickHouseInstallation +# SINGULAR=clickhouseinstallation +# PLURAL=clickhouseinstallations +# SHORT=chi +# OPERATOR_VERSION=0.26.3 +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clickhouseinstallations.clickhouse.altinity.com + labels: + clickhouse.altinity.com/chop: 0.26.3 +spec: + group: clickhouse.altinity.com + scope: Namespaced + names: + kind: ClickHouseInstallation + singular: clickhouseinstallation + plural: clickhouseinstallations + shortNames: + - chi + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: status + type: string + description: Resource status + jsonPath: .status.status + - name: version + type: string + description: Operator version + priority: 1 # show in wide view + jsonPath: .status.chop-version + - name: clusters + type: integer + description: Clusters count + jsonPath: .status.clusters + - name: shards + type: integer + description: Shards count + priority: 1 # show in wide view + jsonPath: .status.shards + - name: hosts + type: integer + description: Hosts count + jsonPath: .status.hosts + - name: taskID + type: string + description: TaskID + priority: 1 # show in wide view + jsonPath: .status.taskID + - name: hosts-completed + type: integer + description: Completed hosts count + jsonPath: .status.hostsCompleted + - name: hosts-updated + type: integer + description: Updated hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsUpdated + - name: hosts-added + type: integer + description: Added hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsAdded + - name: hosts-deleted + type: integer + description: Hosts deleted count + priority: 1 # show in wide view + jsonPath: .status.hostsDeleted + - name: endpoint + type: string + description: Client access endpoint + priority: 1 # show in wide view + jsonPath: .status.endpoint + - name: age + type: date + description: Age of the resource + # Displayed in all priorities + jsonPath: .metadata.creationTimestamp + - name: suspend + type: string + description: Suspend reconciliation + # Displayed in all priorities + jsonPath: .spec.suspend + subresources: + status: {} + schema: + openAPIV3Schema: + description: "define a set of Kubernetes resources (StatefulSet, PVC, Service, ConfigMap) which describe behavior one or more clusters" + type: object + required: + - spec + properties: + apiVersion: + description: | + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: | + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + type: object + description: | + Status contains many fields like a normalized configuration, clickhouse-operator version, current action and all applied action list, current taskID and all applied taskIDs and other + properties: + chop-version: + type: string + description: "Operator version" + chop-commit: + type: string + description: "Operator git commit SHA" + chop-date: + type: string + description: "Operator build date" + chop-ip: + type: string + description: "IP address of the operator's pod which managed this resource" + clusters: + type: integer + minimum: 0 + description: "Clusters count" + shards: + type: integer + minimum: 0 + description: "Shards count" + replicas: + type: integer + minimum: 0 + description: "Replicas count" + hosts: + type: integer + minimum: 0 + description: "Hosts count" + status: + type: string + description: "Status" + taskID: + type: string + description: "Current task id" + taskIDsStarted: + type: array + description: "Started task ids" + nullable: true + items: + type: string + taskIDsCompleted: + type: array + description: "Completed task ids" + nullable: true + items: + type: string + action: + type: string + description: "Action" + actions: + type: array + description: "Actions" + nullable: true + items: + type: string + error: + type: string + description: "Last error" + errors: + type: array + description: "Errors" + nullable: true + items: + type: string + hostsUnchanged: + type: integer + minimum: 0 + description: "Unchanged Hosts count" + hostsUpdated: + type: integer + minimum: 0 + description: "Updated Hosts count" + hostsAdded: + type: integer + minimum: 0 + description: "Added Hosts count" + hostsCompleted: + type: integer + minimum: 0 + description: "Completed Hosts count" + hostsDeleted: + type: integer + minimum: 0 + description: "Deleted Hosts count" + hostsDelete: + type: integer + minimum: 0 + description: "About to delete Hosts count" + pods: + type: array + description: "Pods" + nullable: true + items: + type: string + pod-ips: + type: array + description: "Pod IPs" + nullable: true + items: + type: string + fqdns: + type: array + description: "Pods FQDNs" + nullable: true + items: + type: string + endpoint: + type: string + description: "Endpoint" + endpoints: + type: array + description: "All endpoints" + nullable: true + items: + type: string + generation: + type: integer + minimum: 0 + description: "Generation" + normalized: + type: object + description: "Normalized resource requested" + nullable: true + x-kubernetes-preserve-unknown-fields: true + normalizedCompleted: + type: object + description: "Normalized resource completed" + nullable: true + x-kubernetes-preserve-unknown-fields: true + actionPlan: + type: object + description: "Action Plan" + nullable: true + x-kubernetes-preserve-unknown-fields: true + hostsWithTablesCreated: + type: array + description: "List of hosts with tables created by the operator" + nullable: true + items: + type: string + hostsWithReplicaCaughtUp: + type: array + description: "List of hosts with replica caught up" + nullable: true + items: + type: string + usedTemplates: + type: array + description: "List of templates used to build this CHI" + nullable: true + x-kubernetes-preserve-unknown-fields: true + items: + type: object + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + # x-kubernetes-preserve-unknown-fields: true + description: | + Specification of the desired behavior of one or more ClickHouse clusters + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md + properties: + taskID: + type: string + description: | + Allows to define custom taskID for CHI update and watch status of this update execution. + Displayed in all .status.taskID* fields. + By default (if not filled) every update of CHI manifest will generate random taskID + stop: &TypeStringBool + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + restart: + type: string + description: | + In case 'RollingUpdate' specified, the operator will always restart ClickHouse pods during reconcile. + This options is used in rare cases when force restart is required and is typically removed after the use in order to avoid unneeded restarts. + enum: + - "" + - "RollingUpdate" + suspend: + <<: *TypeStringBool + description: | + Suspend reconciliation of resources managed by a ClickHouse Installation. + Works as the following: + - When `suspend` is `true` operator stops reconciling all resources. + - When `suspend` is `false` or not set, operator reconciles all resources. + troubleshoot: + <<: *TypeStringBool + description: | + Allows to troubleshoot Pods during CrashLoopBack state. + This may happen when wrong configuration applied, in this case `clickhouse-server` wouldn't start. + Command within ClickHouse container is modified with `sleep` in order to avoid quick restarts + and give time to troubleshoot via CLI. + Liveness and Readiness probes are disabled as well. + namespaceDomainPattern: + type: string + description: | + Custom domain pattern which will be used for DNS names of `Service` or `Pod`. + Typical use scenario - custom cluster domain in Kubernetes cluster + Example: %s.svc.my.test + templating: + type: object + # nullable: true + description: | + Optional, applicable inside ClickHouseInstallationTemplate only. + Defines current ClickHouseInstallationTemplate application options to target ClickHouseInstallation(s)." + properties: + policy: + type: string + description: | + When defined as `auto` inside ClickhouseInstallationTemplate, this ClickhouseInstallationTemplate + will be auto-added into ClickHouseInstallation, selectable by `chiSelector`. + Default value is `manual`, meaning ClickHouseInstallation should request this ClickhouseInstallationTemplate explicitly. + enum: + - "" + - "auto" + - "manual" + chiSelector: + type: object + description: "Optional, defines selector for ClickHouseInstallation(s) to be templated with ClickhouseInstallationTemplate" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + reconciling: &TypeReconcile + type: object + description: "[OBSOLETED] Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + # nullable: true + properties: + policy: + type: string + description: | + DISCUSSED TO BE DEPRECATED + Syntax sugar + Overrides all three 'reconcile.host.wait.{exclude, queries, include}' values from the operator's config + Possible values: + - wait - should wait to exclude host, complete queries and include host back into the cluster + - nowait - should NOT wait to exclude host, complete queries and include host back into the cluster + enum: + - "" + - "wait" + - "nowait" + configMapPropagationTimeout: + type: integer + description: | + Timeout in seconds for `clickhouse-operator` to wait for modified `ConfigMap` to propagate into the `Pod` + More details: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically + minimum: 0 + maximum: 3600 + cleanup: + type: object + description: "Optional, defines behavior for cleanup Kubernetes resources during reconcile cycle" + # nullable: true + properties: + unknownObjects: + type: object + description: | + Describes what clickhouse-operator should do with found Kubernetes resources which should be managed by clickhouse-operator, + but do not have `ownerReference` to any currently managed `ClickHouseInstallation` resource. + Default behavior is `Delete`" + # nullable: true + properties: + statefulSet: &TypeObjectsCleanup + type: string + description: "Behavior policy for unknown StatefulSet, `Delete` by default" + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + pvc: + type: string + <<: *TypeObjectsCleanup + description: "Behavior policy for unknown PVC, `Delete` by default" + configMap: + <<: *TypeObjectsCleanup + description: "Behavior policy for unknown ConfigMap, `Delete` by default" + service: + <<: *TypeObjectsCleanup + description: "Behavior policy for unknown Service, `Delete` by default" + reconcileFailedObjects: + type: object + description: | + Describes what clickhouse-operator should do with Kubernetes resources which are failed during reconcile. + Default behavior is `Retain`" + # nullable: true + properties: + statefulSet: + <<: *TypeObjectsCleanup + description: "Behavior policy for failed StatefulSet, `Retain` by default" + pvc: + <<: *TypeObjectsCleanup + description: "Behavior policy for failed PVC, `Retain` by default" + configMap: + <<: *TypeObjectsCleanup + description: "Behavior policy for failed ConfigMap, `Retain` by default" + service: + <<: *TypeObjectsCleanup + description: "Behavior policy for failed Service, `Retain` by default" + macros: + type: object + description: "macros parameters" + properties: + sections: + type: object + description: "sections behaviour for macros" + properties: + users: + type: object + description: "sections behaviour for macros on users" + properties: + enabled: + <<: *TypeStringBool + description: "enabled or not" + profiles: + type: object + description: "sections behaviour for macros on profiles" + properties: + enabled: + <<: *TypeStringBool + description: "enabled or not" + quotas: + type: object + description: "sections behaviour for macros on quotas" + properties: + enabled: + <<: *TypeStringBool + description: "enabled or not" + settings: + type: object + description: "sections behaviour for macros on settings" + properties: + enabled: + <<: *TypeStringBool + description: "enabled or not" + files: + type: object + description: "sections behaviour for macros on files" + properties: + enabled: + <<: *TypeStringBool + description: "enabled or not" + runtime: &TypeReconcileRuntime + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + statefulSet: &TypeReconcileStatefulSet + type: object + description: "Optional, StatefulSet reconcile behavior tuning" + properties: + create: + type: object + description: "Behavior during create StatefulSet" + properties: + onFailure: + type: string + description: | + What to do in case created StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. delete - delete newly created problematic StatefulSet and follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "delete" + - "ignore" + update: + type: object + description: "Behavior during update StatefulSet" + properties: + timeout: + type: integer + description: "How many seconds to wait for StatefulSet to be 'Ready' during update" + minimum: 0 + maximum: 3600 + pollInterval: + type: integer + description: "How many seconds to wait between checks for StatefulSet status during update" + minimum: 1 + maximum: 600 + onFailure: + type: string + description: | + What to do in case updated StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. rollback - delete Pod and rollback StatefulSet to previous Generation. Follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "rollback" + - "ignore" + recreate: + type: object + description: "Behavior during recreate StatefulSet" + properties: + onDataLoss: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to PVC data loss or missing volumes. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + onUpdateFailure: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to update failure or StatefulSet not ready. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + host: &TypeReconcileHost + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + <<: *TypeStringBool + queries: + <<: *TypeStringBool + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + <<: *TypeStringBool + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + <<: *TypeStringBool + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + <<: *TypeStringBool + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + <<: *TypeStringBool + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + <<: *TypeStringBool + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + <<: *TypeStringBool + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + <<: *TypeStringBool + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + <<: *TypeStringBool + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + reconcile: + <<: *TypeReconcile + description: "Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + defaults: + type: object + description: | + define default behavior for whole ClickHouseInstallation, some behavior can be re-define on cluster, shard and replica level + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specdefaults + # nullable: true + properties: + replicasUseFQDN: + <<: *TypeStringBool + description: | + define should replicas be specified by FQDN in `<host></host>`. + In case of "no" will use short hostname and clickhouse-server will use kubernetes default suffixes for DNS lookup + "no" by default + distributedDDL: + type: object + description: | + allows change `<yandex><distributed_ddl></distributed_ddl></yandex>` settings + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings-distributed_ddl + # nullable: true + properties: + profile: + type: string + description: "Settings from this profile will be used to execute DDL queries" + storageManagement: + type: object + description: default storage management options + properties: + provisioner: &TypePVCProvisioner + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: &TypePVCReclaimPolicy + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + templates: &TypeTemplateNames + type: object + description: "optional, configuration of the templates names which will use for generate Kubernetes resources according to one or more ClickHouse clusters described in current ClickHouseInstallation (chi) resource" + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + configuration: + type: object + description: "allows configure multiple aspects and behavior for `clickhouse-server` instance and also allows describe multiple `clickhouse-server` clusters inside one `chi` resource" + # nullable: true + properties: + zookeeper: &TypeZookeeperConfig + type: object + description: | + allows configure <yandex><zookeeper>..</zookeeper></yandex> section in each `Pod` during generate `ConfigMap` which will mounted in `/etc/clickhouse-server/config.d/` + `clickhouse-operator` itself doesn't manage Zookeeper, please install Zookeeper separatelly look examples on https://github.com/Altinity/clickhouse-operator/tree/master/deploy/zookeeper/ + currently, zookeeper (or clickhouse-keeper replacement) used for *ReplicatedMergeTree table engines and for `distributed_ddl` + More details: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings_zookeeper + # nullable: true + properties: + nodes: + type: array + description: "describe every available zookeeper cluster node for interaction" + # nullable: true + items: + type: object + #required: + # - host + properties: + host: + type: string + description: "dns name or ip address for Zookeeper node" + port: + type: integer + description: "TCP port which used to connect to Zookeeper node" + minimum: 0 + maximum: 65535 + secure: + <<: *TypeStringBool + description: "if a secure connection to Zookeeper is required" + availabilityZone: + type: string + description: "availability zone for Zookeeper node" + session_timeout_ms: + type: integer + description: "session timeout during connect to Zookeeper" + operation_timeout_ms: + type: integer + description: "one operation timeout during Zookeeper transactions" + root: + type: string + description: "optional root znode path inside zookeeper to store ClickHouse related data (replication queue or distributed DDL)" + identity: + type: string + description: "optional access credentials string with `user:password` format used when use digest authorization in Zookeeper" + use_compression: + !!merge <<: *TypeStringBool + description: "Enables compression in Keeper protocol if set to true" + users: + type: object + description: | + allows configure <yandex><users>..</users></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure password hashed, authorization restrictions, database level security row filters etc. + More details: https://clickhouse.tech/docs/en/operations/settings/settings-users/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationusers + + any key could contains `valueFrom` with `secretKeyRef` which allow pass password from kubernetes secrets + secret value will pass in `pod.spec.containers.evn`, and generate with from_env=XXX in XML in /etc/clickhouse-server/users.d/chop-generated-users.xml + it not allow automatically updates when updates `secret`, change spec.taskID for manually trigger reconcile cycle + + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + any key with prefix `k8s_secret_` shall has value with format namespace/secret/key or secret/key + in this case value from secret will write directly into XML tag during render *-usersd ConfigMap + + any key with prefix `k8s_secret_env` shall has value with format namespace/secret/key or secret/key + in this case value from secret will write into environment variable and write to XML tag via from_env=XXX + + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + # nullable: true + x-kubernetes-preserve-unknown-fields: true + profiles: + type: object + description: | + allows configure <yandex><profiles>..</profiles></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure any aspect of settings profile + More details: https://clickhouse.tech/docs/en/operations/settings/settings-profiles/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationprofiles + # nullable: true + x-kubernetes-preserve-unknown-fields: true + quotas: + type: object + description: | + allows configure <yandex><quotas>..</quotas></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure any aspect of resource quotas + More details: https://clickhouse.tech/docs/en/operations/quotas/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationquotas + # nullable: true + x-kubernetes-preserve-unknown-fields: true + settings: &TypeSettings + type: object + description: | + allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationsettings + + any key could contains `valueFrom` with `secretKeyRef` which allow pass password from kubernetes secrets + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + secret value will pass in `pod.spec.env`, and generate with from_env=XXX in XML in /etc/clickhouse-server/config.d/chop-generated-settings.xml + it not allow automatically updates when updates `secret`, change spec.taskID for manually trigger reconcile cycle + + # nullable: true + x-kubernetes-preserve-unknown-fields: true + files: &TypeFiles + type: object + description: | + allows define content of any setting file inside each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + every key in this object is the file name + every value in this object is the file content + you can use `!!binary |` and base64 for binary files, see details here https://yaml.org/type/binary.html + each key could contains prefix like {common}, {users}, {hosts} or config.d, users.d, conf.d, wrong prefixes will be ignored, subfolders also will be ignored + More details: https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-05-files-nested.yaml + + any key could contains `valueFrom` with `secretKeyRef` which allow pass values from kubernetes secrets + secrets will mounted into pod as separate volume in /etc/clickhouse-server/secrets.d/ + and will automatically update when update secret + it useful for pass SSL certificates from cert-manager or similar tool + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + # nullable: true + x-kubernetes-preserve-unknown-fields: true + clusters: + type: array + description: | + describes clusters layout and allows change settings on cluster-level, shard-level and replica-level + every cluster is a set of StatefulSet, one StatefulSet contains only one Pod with `clickhouse-server` + all Pods will rendered in <remote_server> part of ClickHouse configs, mounted from ConfigMap as `/etc/clickhouse-server/config.d/chop-generated-remote_servers.xml` + Clusters will use for Distributed table engine, more details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + If `cluster` contains zookeeper settings (could be inherited from top `chi` level), when you can create *ReplicatedMergeTree tables + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "cluster name, used to identify set of servers and wide used during generate names of related Kubernetes resources" + minLength: 1 + # See namePartClusterMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + zookeeper: + <<: *TypeZookeeperConfig + description: | + optional, allows configure <yandex><zookeeper>..</zookeeper></yandex> section in each `Pod` only in current ClickHouse cluster, during generate `ConfigMap` which will mounted in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.zookeeper` settings + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` on current cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` + templates: + <<: *TypeTemplateNames + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected cluster + override top-level `chi.spec.configuration.templates` + schemaPolicy: + type: object + description: | + describes how schema is propagated within replicas and shards + properties: + replica: + type: string + description: "how schema is propagated within a replica" + enum: + # List SchemaPolicyReplicaXXX constants from model + - "" + - "None" + - "All" + shard: + type: string + description: "how schema is propagated between shards" + enum: + # List SchemaPolicyShardXXX constants from model + - "" + - "None" + - "All" + - "DistributedTablesOnly" + insecure: + <<: *TypeStringBool + description: optional, open insecure ports for cluster, defaults to "yes" + secure: + <<: *TypeStringBool + description: optional, open secure ports for cluster + secret: + type: object + description: "optional, shared secret value to secure cluster communications" + properties: + auto: + <<: *TypeStringBool + description: "Auto-generate shared secret value to secure cluster communications" + value: + description: "Cluster shared secret value in plain text" + type: string + valueFrom: + description: "Cluster shared secret source" + type: object + properties: + secretKeyRef: + description: | + Selects a key of a secret in the clickhouse installation namespace. + Should not be used if value is not empty. + type: object + properties: + name: + description: | + Name of the referent. More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + key: + description: The key of the secret to select from. Must be a valid secret key. + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - name + - key + pdbManaged: + <<: *TypeStringBool + description: | + Specifies whether the Pod Disruption Budget (PDB) should be managed. + During the next installation, if PDB management is enabled, the operator will + attempt to retrieve any existing PDB. If none is found, it will create a new one + and initiate a reconciliation loop. If PDB management is disabled, the existing PDB + will remain intact, and the reconciliation loop will not be executed. By default, + PDB management is enabled. + pdbMaxUnavailable: + type: integer + description: | + Pod eviction is allowed if at most "pdbMaxUnavailable" pods are unavailable after the eviction, + i.e. even in absence of the evicted pod. For example, one can prevent all voluntary evictions + by specifying 0. This is a mutually exclusive setting with "minAvailable". + minimum: 0 + maximum: 65535 + reconcile: + type: object + description: "allow tuning reconciling process" + properties: + runtime: + <<: *TypeReconcileRuntime + host: + <<: *TypeReconcileHost + layout: + type: object + description: | + describe current cluster layout, how much shards in cluster, how much replica in shard + allows override settings on each shard and replica separatelly + # nullable: true + properties: + shardsCount: + type: integer + description: | + how much shards for current ClickHouse cluster will run in Kubernetes, + each shard contains shared-nothing part of data and contains set of replicas, + cluster contains 1 shard by default" + replicasCount: + type: integer + description: | + how much replicas in each shards for current cluster will run in Kubernetes, + each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + every shard contains 1 replica by default" + shards: + type: array + description: | + optional, allows override top-level `chi.spec.configuration`, cluster-level + `chi.spec.configuration.clusters` settings for each shard separately, + use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + definitionType: + type: string + description: "DEPRECATED - to be removed soon" + weight: + type: integer + description: | + optional, 1 by default, allows setup shard <weight> setting which will use during insert into tables with `Distributed` engine, + will apply in <remote_servers> inside ConfigMap which will mount in /etc/clickhouse-server/config.d/chop-generated-remote_servers.xml + More details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + internalReplication: + <<: *TypeStringBool + description: | + optional, `true` by default when `chi.spec.configuration.clusters[].layout.ReplicaCount` > 1 and 0 otherwise + allows setup <internal_replication> setting which will use during insert into tables with `Distributed` engine for insert only in one live replica and other replicas will download inserted data during replication, + will apply in <remote_servers> inside ConfigMap which will mount in /etc/clickhouse-server/config.d/chop-generated-remote_servers.xml + More details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one shard during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` and cluster-level `chi.spec.configuration.clusters.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files` + templates: + <<: *TypeTemplateNames + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected shard + override top-level `chi.spec.configuration.templates` and cluster-level `chi.spec.configuration.clusters.templates` + replicasCount: + type: integer + description: | + optional, how much replicas in selected shard for selected ClickHouse cluster will run in Kubernetes, each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + shard contains 1 replica by default + override cluster-level `chi.spec.configuration.clusters.layout.replicasCount` + minimum: 1 + replicas: + type: array + description: | + optional, allows override behavior for selected replicas from cluster-level `chi.spec.configuration.clusters` and shard-level `chi.spec.configuration.clusters.layout.shards` + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + <<: *TypeStringBool + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + <<: *TypeStringBool + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `tcp` for selected replica, override `chi.spec.templates.hostTemplates.spec.tcpPort` + allows connect to `clickhouse-server` via TCP Native protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `http` for selected replica, override `chi.spec.templates.hostTemplates.spec.httpPort` + allows connect to `clickhouse-server` via HTTP protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `interserver` for selected replica, override `chi.spec.templates.hostTemplates.spec.interserverHTTPPort` + allows connect between replicas inside same shard during fetch replicated data parts HTTP protocol + minimum: 1 + maximum: 65535 + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and shard-level `chi.spec.configuration.clusters.layout.shards.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files`, cluster-level `chi.spec.configuration.clusters.files` and shard-level `chi.spec.configuration.clusters.layout.shards.files` + templates: + <<: *TypeTemplateNames + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` and shard-level `chi.spec.configuration.clusters.layout.shards.templates` + replicas: + type: array + description: "optional, allows override top-level `chi.spec.configuration` and cluster-level `chi.spec.configuration.clusters` configuration for each replica and each shard relates to selected replica, use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and will ignore if shard-level `chi.spec.configuration.clusters.layout.shards` present + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + <<: *TypeTemplateNames + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` + shardsCount: + type: integer + description: "optional, count of shards related to current replica, you can override each shard behavior on low-level `chi.spec.configuration.clusters.layout.replicas.shards`" + minimum: 1 + shards: + type: array + description: "optional, list of shards related to current replica, will ignore if `chi.spec.configuration.clusters.layout.shards` presents" + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + <<: *TypeStringBool + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + <<: *TypeStringBool + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `tcp` for selected shard, override `chi.spec.templates.hostTemplates.spec.tcpPort` + allows connect to `clickhouse-server` via TCP Native protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `http` for selected shard, override `chi.spec.templates.hostTemplates.spec.httpPort` + allows connect to `clickhouse-server` via HTTP protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `interserver` for selected shard, override `chi.spec.templates.hostTemplates.spec.interserverHTTPPort` + allows connect between replicas inside same shard during fetch replicated data parts HTTP protocol + minimum: 1 + maximum: 65535 + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and replica-level `chi.spec.configuration.clusters.layout.replicas.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + <<: *TypeTemplateNames + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates` + templates: + type: object + description: "allows define templates which will use for render Kubernetes resources like StatefulSet, ConfigMap, Service, PVC, by default, clickhouse-operator have own templates, but you can override it" + # nullable: true + properties: + hostTemplates: + type: array + description: "hostTemplate will use during apply to generate `clickhose-server` config files" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.hostTemplate`, cluster-level `chi.spec.configuration.clusters.templates.hostTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.hostTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.hostTemplate`" + type: string + portDistribution: + type: array + description: "define how will distribute numeric values of named ports in `Pod.spec.containers.ports` and clickhouse-server configs" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "type of distribution, when `Unspecified` (default value) then all listen ports on clickhouse-server configuration in all Pods will have the same value, when `ClusterScopeIndex` then ports will increment to offset from base value depends on shard and replica index inside cluster with combination of `chi.spec.templates.podTemlates.spec.HostNetwork` it allows setup ClickHouse cluster inside Kubernetes and provide access via external network bypass Kubernetes internal network" + enum: + # List PortDistributionXXX constants + - "" + - "Unspecified" + - "ClusterScopeIndex" + spec: + # Host + type: object + properties: + name: + type: string + description: "by default, hostname will generate, but this allows define custom name for each `clickhouse-server`" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + <<: *TypeStringBool + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + <<: *TypeStringBool + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `tcp_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=tcp]` + More info: https://clickhouse.tech/docs/en/interfaces/tcp/ + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `http_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=http]` + More info: https://clickhouse.tech/docs/en/interfaces/http/ + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `interserver_http_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=interserver]` + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#interserver-http-port + minimum: 1 + maximum: 65535 + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + templates: + <<: *TypeTemplateNames + description: "be careful, this part of CRD allows override template inside template, don't use it if you don't understand what you do" + + podTemplates: + type: array + description: | + podTemplate will use during render `Pod` inside `StatefulSet.spec` and allows define rendered `Pod.spec`, pod scheduling distribution and pod zone + More information: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatespodtemplates + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.podTemplate`, cluster-level `chi.spec.configuration.clusters.templates.podTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.podTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.podTemplate`" + generateName: + type: string + description: "allows define format for generated `Pod` name, look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates for details about available template variables" + zone: + type: object + description: "allows define custom zone name and will separate ClickHouse `Pods` between nodes, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + #required: + # - values + properties: + key: + type: string + description: "optional, if defined, allows select kubernetes nodes by label with `name` equal `key`" + values: + type: array + description: "optional, if defined, allows select kubernetes nodes by label with `value` in `values`" + # nullable: true + items: + type: string + distribution: + type: string + description: "DEPRECATED, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + enum: + - "" + - "Unspecified" + - "OnePerHost" + podDistribution: + type: array + description: "define ClickHouse Pod distribution policy between Kubernetes Nodes inside Shard, Replica, Namespace, CHI, another ClickHouse cluster" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "you can define multiple affinity policy types" + enum: + # List PodDistributionXXX constants + - "" + - "Unspecified" + - "ClickHouseAntiAffinity" + - "ShardAntiAffinity" + - "ReplicaAntiAffinity" + - "AnotherNamespaceAntiAffinity" + - "AnotherClickHouseInstallationAntiAffinity" + - "AnotherClusterAntiAffinity" + - "MaxNumberPerNode" + - "NamespaceAffinity" + - "ClickHouseInstallationAffinity" + - "ClusterAffinity" + - "ShardAffinity" + - "ReplicaAffinity" + - "PreviousTailAffinity" + - "CircularReplication" + scope: + type: string + description: "scope for apply each podDistribution" + enum: + # list PodDistributionScopeXXX constants + - "" + - "Unspecified" + - "Shard" + - "Replica" + - "Cluster" + - "ClickHouseInstallation" + - "Namespace" + number: + type: integer + description: "define, how much ClickHouse Pods could be inside selected scope with selected distribution type" + minimum: 0 + maximum: 65535 + topologyKey: + type: string + description: | + use for inter-pod affinity look to `pod.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution.podAffinityTerm.topologyKey`, + more info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity" + metadata: + type: object + description: | + allows pass standard object's metadata from template to Pod + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify PodSpec + type: object + description: "allows define whole Pod.spec inside StaefulSet.spec, look to https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates for details" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + + volumeClaimTemplates: + type: array + description: | + allows define template for rendering `PVC` kubernetes resource, which would use inside `Pod` for mount clickhouse `data`, clickhouse `logs` or something else + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + top-level `chi.spec.defaults.templates.dataVolumeClaimTemplate` or `chi.spec.defaults.templates.logVolumeClaimTemplate`, + cluster-level `chi.spec.configuration.clusters.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.templates.logVolumeClaimTemplate`, + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.shards.temlates.logVolumeClaimTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.replicas.templates.logVolumeClaimTemplate` + provisioner: *TypePVCProvisioner + reclaimPolicy: *TypePVCReclaimPolicy + metadata: + type: object + description: | + allows to pass standard object's metadata from template to PVC + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + description: | + allows define all aspects of `PVC` resource + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims + # nullable: true + x-kubernetes-preserve-unknown-fields: true + serviceTemplates: + type: array + description: | + allows define template for rendering `Service` which would get endpoint from Pods which scoped chi-wide, cluster-wide, shard-wide, replica-wide level + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + chi-level `chi.spec.defaults.templates.serviceTemplate` + cluster-level `chi.spec.configuration.clusters.templates.clusterServiceTemplate` + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.shardServiceTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.replicaServiceTemplate` or `chi.spec.configuration.clusters.layout.shards.replicas.replicaServiceTemplate` + generateName: + type: string + description: | + allows define format for generated `Service` name, + look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates + for details about available template variables" + metadata: + # TODO specify ObjectMeta + type: object + description: | + allows pass standard object's metadata from template to Service + Could be use for define specificly for Cloud Provider metadata which impact to behavior of service + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify ServiceSpec + type: object + description: | + describe behavior of generated Service + More info: https://kubernetes.io/docs/concepts/services-networking/service/ + # nullable: true + x-kubernetes-preserve-unknown-fields: true + useTemplates: + type: array + description: | + list of `ClickHouseInstallationTemplate` (chit) resource names which will merge with current `CHI` + manifest during render Kubernetes resources to create related ClickHouse clusters" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "name of `ClickHouseInstallationTemplate` (chit) resource" + namespace: + type: string + description: "Kubernetes namespace where need search `chit` resource, depending on `watchNamespaces` settings in `clickhouse-operator`" + useType: + type: string + description: "optional, current strategy is only merge, and current `chi` settings have more priority than merged template `chit`" + enum: + # List useTypeXXX constants from model + - "" + - "merge" +--- +# Template Parameters: +# +# KIND=ClickHouseInstallationTemplate +# SINGULAR=clickhouseinstallationtemplate +# PLURAL=clickhouseinstallationtemplates +# SHORT=chit +# OPERATOR_VERSION=0.26.3 +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clickhouseinstallationtemplates.clickhouse.altinity.com + labels: + clickhouse.altinity.com/chop: 0.26.3 +spec: + group: clickhouse.altinity.com + scope: Namespaced + names: + kind: ClickHouseInstallationTemplate + singular: clickhouseinstallationtemplate + plural: clickhouseinstallationtemplates + shortNames: + - chit + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: status + type: string + description: Resource status + jsonPath: .status.status + - name: version + type: string + description: Operator version + priority: 1 # show in wide view + jsonPath: .status.chop-version + - name: clusters + type: integer + description: Clusters count + jsonPath: .status.clusters + - name: shards + type: integer + description: Shards count + priority: 1 # show in wide view + jsonPath: .status.shards + - name: hosts + type: integer + description: Hosts count + jsonPath: .status.hosts + - name: taskID + type: string + description: TaskID + priority: 1 # show in wide view + jsonPath: .status.taskID + - name: hosts-completed + type: integer + description: Completed hosts count + jsonPath: .status.hostsCompleted + - name: hosts-updated + type: integer + description: Updated hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsUpdated + - name: hosts-added + type: integer + description: Added hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsAdded + - name: hosts-deleted + type: integer + description: Hosts deleted count + priority: 1 # show in wide view + jsonPath: .status.hostsDeleted + - name: endpoint + type: string + description: Client access endpoint + priority: 1 # show in wide view + jsonPath: .status.endpoint + - name: age + type: date + description: Age of the resource + # Displayed in all priorities + jsonPath: .metadata.creationTimestamp + - name: suspend + type: string + description: Suspend reconciliation + # Displayed in all priorities + jsonPath: .spec.suspend + subresources: + status: {} + schema: + openAPIV3Schema: + description: "define a set of Kubernetes resources (StatefulSet, PVC, Service, ConfigMap) which describe behavior one or more clusters" + type: object + required: + - spec + properties: + apiVersion: + description: | + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: | + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + type: object + description: | + Status contains many fields like a normalized configuration, clickhouse-operator version, current action and all applied action list, current taskID and all applied taskIDs and other + properties: + chop-version: + type: string + description: "Operator version" + chop-commit: + type: string + description: "Operator git commit SHA" + chop-date: + type: string + description: "Operator build date" + chop-ip: + type: string + description: "IP address of the operator's pod which managed this resource" + clusters: + type: integer + minimum: 0 + description: "Clusters count" + shards: + type: integer + minimum: 0 + description: "Shards count" + replicas: + type: integer + minimum: 0 + description: "Replicas count" + hosts: + type: integer + minimum: 0 + description: "Hosts count" + status: + type: string + description: "Status" + taskID: + type: string + description: "Current task id" + taskIDsStarted: + type: array + description: "Started task ids" + nullable: true + items: + type: string + taskIDsCompleted: + type: array + description: "Completed task ids" + nullable: true + items: + type: string + action: + type: string + description: "Action" + actions: + type: array + description: "Actions" + nullable: true + items: + type: string + error: + type: string + description: "Last error" + errors: + type: array + description: "Errors" + nullable: true + items: + type: string + hostsUnchanged: + type: integer + minimum: 0 + description: "Unchanged Hosts count" + hostsUpdated: + type: integer + minimum: 0 + description: "Updated Hosts count" + hostsAdded: + type: integer + minimum: 0 + description: "Added Hosts count" + hostsCompleted: + type: integer + minimum: 0 + description: "Completed Hosts count" + hostsDeleted: + type: integer + minimum: 0 + description: "Deleted Hosts count" + hostsDelete: + type: integer + minimum: 0 + description: "About to delete Hosts count" + pods: + type: array + description: "Pods" + nullable: true + items: + type: string + pod-ips: + type: array + description: "Pod IPs" + nullable: true + items: + type: string + fqdns: + type: array + description: "Pods FQDNs" + nullable: true + items: + type: string + endpoint: + type: string + description: "Endpoint" + endpoints: + type: array + description: "All endpoints" + nullable: true + items: + type: string + generation: + type: integer + minimum: 0 + description: "Generation" + normalized: + type: object + description: "Normalized resource requested" + nullable: true + x-kubernetes-preserve-unknown-fields: true + normalizedCompleted: + type: object + description: "Normalized resource completed" + nullable: true + x-kubernetes-preserve-unknown-fields: true + actionPlan: + type: object + description: "Action Plan" + nullable: true + x-kubernetes-preserve-unknown-fields: true + hostsWithTablesCreated: + type: array + description: "List of hosts with tables created by the operator" + nullable: true + items: + type: string + hostsWithReplicaCaughtUp: + type: array + description: "List of hosts with replica caught up" + nullable: true + items: + type: string + usedTemplates: + type: array + description: "List of templates used to build this CHI" + nullable: true + x-kubernetes-preserve-unknown-fields: true + items: + type: object + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + # x-kubernetes-preserve-unknown-fields: true + description: | + Specification of the desired behavior of one or more ClickHouse clusters + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md + properties: + taskID: + type: string + description: | + Allows to define custom taskID for CHI update and watch status of this update execution. + Displayed in all .status.taskID* fields. + By default (if not filled) every update of CHI manifest will generate random taskID + stop: &TypeStringBool + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + restart: + type: string + description: | + In case 'RollingUpdate' specified, the operator will always restart ClickHouse pods during reconcile. + This options is used in rare cases when force restart is required and is typically removed after the use in order to avoid unneeded restarts. + enum: + - "" + - "RollingUpdate" + suspend: + <<: *TypeStringBool + description: | + Suspend reconciliation of resources managed by a ClickHouse Installation. + Works as the following: + - When `suspend` is `true` operator stops reconciling all resources. + - When `suspend` is `false` or not set, operator reconciles all resources. + troubleshoot: + <<: *TypeStringBool + description: | + Allows to troubleshoot Pods during CrashLoopBack state. + This may happen when wrong configuration applied, in this case `clickhouse-server` wouldn't start. + Command within ClickHouse container is modified with `sleep` in order to avoid quick restarts + and give time to troubleshoot via CLI. + Liveness and Readiness probes are disabled as well. + namespaceDomainPattern: + type: string + description: | + Custom domain pattern which will be used for DNS names of `Service` or `Pod`. + Typical use scenario - custom cluster domain in Kubernetes cluster + Example: %s.svc.my.test + templating: + type: object + # nullable: true + description: | + Optional, applicable inside ClickHouseInstallationTemplate only. + Defines current ClickHouseInstallationTemplate application options to target ClickHouseInstallation(s)." + properties: + policy: + type: string + description: | + When defined as `auto` inside ClickhouseInstallationTemplate, this ClickhouseInstallationTemplate + will be auto-added into ClickHouseInstallation, selectable by `chiSelector`. + Default value is `manual`, meaning ClickHouseInstallation should request this ClickhouseInstallationTemplate explicitly. + enum: + - "" + - "auto" + - "manual" + chiSelector: + type: object + description: "Optional, defines selector for ClickHouseInstallation(s) to be templated with ClickhouseInstallationTemplate" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + reconciling: &TypeReconcile + type: object + description: "[OBSOLETED] Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + # nullable: true + properties: + policy: + type: string + description: | + DISCUSSED TO BE DEPRECATED + Syntax sugar + Overrides all three 'reconcile.host.wait.{exclude, queries, include}' values from the operator's config + Possible values: + - wait - should wait to exclude host, complete queries and include host back into the cluster + - nowait - should NOT wait to exclude host, complete queries and include host back into the cluster + enum: + - "" + - "wait" + - "nowait" + configMapPropagationTimeout: + type: integer + description: | + Timeout in seconds for `clickhouse-operator` to wait for modified `ConfigMap` to propagate into the `Pod` + More details: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically + minimum: 0 + maximum: 3600 + cleanup: + type: object + description: "Optional, defines behavior for cleanup Kubernetes resources during reconcile cycle" + # nullable: true + properties: + unknownObjects: + type: object + description: | + Describes what clickhouse-operator should do with found Kubernetes resources which should be managed by clickhouse-operator, + but do not have `ownerReference` to any currently managed `ClickHouseInstallation` resource. + Default behavior is `Delete`" + # nullable: true + properties: + statefulSet: &TypeObjectsCleanup + type: string + description: "Behavior policy for unknown StatefulSet, `Delete` by default" + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + pvc: + type: string + <<: *TypeObjectsCleanup + description: "Behavior policy for unknown PVC, `Delete` by default" + configMap: + <<: *TypeObjectsCleanup + description: "Behavior policy for unknown ConfigMap, `Delete` by default" + service: + <<: *TypeObjectsCleanup + description: "Behavior policy for unknown Service, `Delete` by default" + reconcileFailedObjects: + type: object + description: | + Describes what clickhouse-operator should do with Kubernetes resources which are failed during reconcile. + Default behavior is `Retain`" + # nullable: true + properties: + statefulSet: + <<: *TypeObjectsCleanup + description: "Behavior policy for failed StatefulSet, `Retain` by default" + pvc: + <<: *TypeObjectsCleanup + description: "Behavior policy for failed PVC, `Retain` by default" + configMap: + <<: *TypeObjectsCleanup + description: "Behavior policy for failed ConfigMap, `Retain` by default" + service: + <<: *TypeObjectsCleanup + description: "Behavior policy for failed Service, `Retain` by default" + macros: + type: object + description: "macros parameters" + properties: + sections: + type: object + description: "sections behaviour for macros" + properties: + users: + type: object + description: "sections behaviour for macros on users" + properties: + enabled: + <<: *TypeStringBool + description: "enabled or not" + profiles: + type: object + description: "sections behaviour for macros on profiles" + properties: + enabled: + <<: *TypeStringBool + description: "enabled or not" + quotas: + type: object + description: "sections behaviour for macros on quotas" + properties: + enabled: + <<: *TypeStringBool + description: "enabled or not" + settings: + type: object + description: "sections behaviour for macros on settings" + properties: + enabled: + <<: *TypeStringBool + description: "enabled or not" + files: + type: object + description: "sections behaviour for macros on files" + properties: + enabled: + <<: *TypeStringBool + description: "enabled or not" + runtime: &TypeReconcileRuntime + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + statefulSet: &TypeReconcileStatefulSet + type: object + description: "Optional, StatefulSet reconcile behavior tuning" + properties: + create: + type: object + description: "Behavior during create StatefulSet" + properties: + onFailure: + type: string + description: | + What to do in case created StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. delete - delete newly created problematic StatefulSet and follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "delete" + - "ignore" + update: + type: object + description: "Behavior during update StatefulSet" + properties: + timeout: + type: integer + description: "How many seconds to wait for StatefulSet to be 'Ready' during update" + minimum: 0 + maximum: 3600 + pollInterval: + type: integer + description: "How many seconds to wait between checks for StatefulSet status during update" + minimum: 1 + maximum: 600 + onFailure: + type: string + description: | + What to do in case updated StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. rollback - delete Pod and rollback StatefulSet to previous Generation. Follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "rollback" + - "ignore" + recreate: + type: object + description: "Behavior during recreate StatefulSet" + properties: + onDataLoss: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to PVC data loss or missing volumes. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + onUpdateFailure: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to update failure or StatefulSet not ready. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + host: &TypeReconcileHost + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + <<: *TypeStringBool + queries: + <<: *TypeStringBool + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + <<: *TypeStringBool + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + <<: *TypeStringBool + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + <<: *TypeStringBool + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + <<: *TypeStringBool + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + <<: *TypeStringBool + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + <<: *TypeStringBool + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + <<: *TypeStringBool + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + <<: *TypeStringBool + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + reconcile: + <<: *TypeReconcile + description: "Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + defaults: + type: object + description: | + define default behavior for whole ClickHouseInstallation, some behavior can be re-define on cluster, shard and replica level + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specdefaults + # nullable: true + properties: + replicasUseFQDN: + <<: *TypeStringBool + description: | + define should replicas be specified by FQDN in `<host></host>`. + In case of "no" will use short hostname and clickhouse-server will use kubernetes default suffixes for DNS lookup + "no" by default + distributedDDL: + type: object + description: | + allows change `<yandex><distributed_ddl></distributed_ddl></yandex>` settings + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings-distributed_ddl + # nullable: true + properties: + profile: + type: string + description: "Settings from this profile will be used to execute DDL queries" + storageManagement: + type: object + description: default storage management options + properties: + provisioner: &TypePVCProvisioner + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: &TypePVCReclaimPolicy + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + templates: &TypeTemplateNames + type: object + description: "optional, configuration of the templates names which will use for generate Kubernetes resources according to one or more ClickHouse clusters described in current ClickHouseInstallation (chi) resource" + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + configuration: + type: object + description: "allows configure multiple aspects and behavior for `clickhouse-server` instance and also allows describe multiple `clickhouse-server` clusters inside one `chi` resource" + # nullable: true + properties: + zookeeper: &TypeZookeeperConfig + type: object + description: | + allows configure <yandex><zookeeper>..</zookeeper></yandex> section in each `Pod` during generate `ConfigMap` which will mounted in `/etc/clickhouse-server/config.d/` + `clickhouse-operator` itself doesn't manage Zookeeper, please install Zookeeper separatelly look examples on https://github.com/Altinity/clickhouse-operator/tree/master/deploy/zookeeper/ + currently, zookeeper (or clickhouse-keeper replacement) used for *ReplicatedMergeTree table engines and for `distributed_ddl` + More details: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings_zookeeper + # nullable: true + properties: + nodes: + type: array + description: "describe every available zookeeper cluster node for interaction" + # nullable: true + items: + type: object + #required: + # - host + properties: + host: + type: string + description: "dns name or ip address for Zookeeper node" + port: + type: integer + description: "TCP port which used to connect to Zookeeper node" + minimum: 0 + maximum: 65535 + secure: + <<: *TypeStringBool + description: "if a secure connection to Zookeeper is required" + availabilityZone: + type: string + description: "availability zone for Zookeeper node" + session_timeout_ms: + type: integer + description: "session timeout during connect to Zookeeper" + operation_timeout_ms: + type: integer + description: "one operation timeout during Zookeeper transactions" + root: + type: string + description: "optional root znode path inside zookeeper to store ClickHouse related data (replication queue or distributed DDL)" + identity: + type: string + description: "optional access credentials string with `user:password` format used when use digest authorization in Zookeeper" + use_compression: + !!merge <<: *TypeStringBool + description: "Enables compression in Keeper protocol if set to true" + users: + type: object + description: | + allows configure <yandex><users>..</users></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure password hashed, authorization restrictions, database level security row filters etc. + More details: https://clickhouse.tech/docs/en/operations/settings/settings-users/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationusers + + any key could contains `valueFrom` with `secretKeyRef` which allow pass password from kubernetes secrets + secret value will pass in `pod.spec.containers.evn`, and generate with from_env=XXX in XML in /etc/clickhouse-server/users.d/chop-generated-users.xml + it not allow automatically updates when updates `secret`, change spec.taskID for manually trigger reconcile cycle + + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + any key with prefix `k8s_secret_` shall has value with format namespace/secret/key or secret/key + in this case value from secret will write directly into XML tag during render *-usersd ConfigMap + + any key with prefix `k8s_secret_env` shall has value with format namespace/secret/key or secret/key + in this case value from secret will write into environment variable and write to XML tag via from_env=XXX + + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + # nullable: true + x-kubernetes-preserve-unknown-fields: true + profiles: + type: object + description: | + allows configure <yandex><profiles>..</profiles></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure any aspect of settings profile + More details: https://clickhouse.tech/docs/en/operations/settings/settings-profiles/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationprofiles + # nullable: true + x-kubernetes-preserve-unknown-fields: true + quotas: + type: object + description: | + allows configure <yandex><quotas>..</quotas></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure any aspect of resource quotas + More details: https://clickhouse.tech/docs/en/operations/quotas/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationquotas + # nullable: true + x-kubernetes-preserve-unknown-fields: true + settings: &TypeSettings + type: object + description: | + allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationsettings + + any key could contains `valueFrom` with `secretKeyRef` which allow pass password from kubernetes secrets + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + secret value will pass in `pod.spec.env`, and generate with from_env=XXX in XML in /etc/clickhouse-server/config.d/chop-generated-settings.xml + it not allow automatically updates when updates `secret`, change spec.taskID for manually trigger reconcile cycle + + # nullable: true + x-kubernetes-preserve-unknown-fields: true + files: &TypeFiles + type: object + description: | + allows define content of any setting file inside each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + every key in this object is the file name + every value in this object is the file content + you can use `!!binary |` and base64 for binary files, see details here https://yaml.org/type/binary.html + each key could contains prefix like {common}, {users}, {hosts} or config.d, users.d, conf.d, wrong prefixes will be ignored, subfolders also will be ignored + More details: https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-05-files-nested.yaml + + any key could contains `valueFrom` with `secretKeyRef` which allow pass values from kubernetes secrets + secrets will mounted into pod as separate volume in /etc/clickhouse-server/secrets.d/ + and will automatically update when update secret + it useful for pass SSL certificates from cert-manager or similar tool + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + # nullable: true + x-kubernetes-preserve-unknown-fields: true + clusters: + type: array + description: | + describes clusters layout and allows change settings on cluster-level, shard-level and replica-level + every cluster is a set of StatefulSet, one StatefulSet contains only one Pod with `clickhouse-server` + all Pods will rendered in <remote_server> part of ClickHouse configs, mounted from ConfigMap as `/etc/clickhouse-server/config.d/chop-generated-remote_servers.xml` + Clusters will use for Distributed table engine, more details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + If `cluster` contains zookeeper settings (could be inherited from top `chi` level), when you can create *ReplicatedMergeTree tables + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "cluster name, used to identify set of servers and wide used during generate names of related Kubernetes resources" + minLength: 1 + # See namePartClusterMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + zookeeper: + <<: *TypeZookeeperConfig + description: | + optional, allows configure <yandex><zookeeper>..</zookeeper></yandex> section in each `Pod` only in current ClickHouse cluster, during generate `ConfigMap` which will mounted in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.zookeeper` settings + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` on current cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` + templates: + <<: *TypeTemplateNames + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected cluster + override top-level `chi.spec.configuration.templates` + schemaPolicy: + type: object + description: | + describes how schema is propagated within replicas and shards + properties: + replica: + type: string + description: "how schema is propagated within a replica" + enum: + # List SchemaPolicyReplicaXXX constants from model + - "" + - "None" + - "All" + shard: + type: string + description: "how schema is propagated between shards" + enum: + # List SchemaPolicyShardXXX constants from model + - "" + - "None" + - "All" + - "DistributedTablesOnly" + insecure: + <<: *TypeStringBool + description: optional, open insecure ports for cluster, defaults to "yes" + secure: + <<: *TypeStringBool + description: optional, open secure ports for cluster + secret: + type: object + description: "optional, shared secret value to secure cluster communications" + properties: + auto: + <<: *TypeStringBool + description: "Auto-generate shared secret value to secure cluster communications" + value: + description: "Cluster shared secret value in plain text" + type: string + valueFrom: + description: "Cluster shared secret source" + type: object + properties: + secretKeyRef: + description: | + Selects a key of a secret in the clickhouse installation namespace. + Should not be used if value is not empty. + type: object + properties: + name: + description: | + Name of the referent. More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + key: + description: The key of the secret to select from. Must be a valid secret key. + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - name + - key + pdbManaged: + <<: *TypeStringBool + description: | + Specifies whether the Pod Disruption Budget (PDB) should be managed. + During the next installation, if PDB management is enabled, the operator will + attempt to retrieve any existing PDB. If none is found, it will create a new one + and initiate a reconciliation loop. If PDB management is disabled, the existing PDB + will remain intact, and the reconciliation loop will not be executed. By default, + PDB management is enabled. + pdbMaxUnavailable: + type: integer + description: | + Pod eviction is allowed if at most "pdbMaxUnavailable" pods are unavailable after the eviction, + i.e. even in absence of the evicted pod. For example, one can prevent all voluntary evictions + by specifying 0. This is a mutually exclusive setting with "minAvailable". + minimum: 0 + maximum: 65535 + reconcile: + type: object + description: "allow tuning reconciling process" + properties: + runtime: + <<: *TypeReconcileRuntime + host: + <<: *TypeReconcileHost + layout: + type: object + description: | + describe current cluster layout, how much shards in cluster, how much replica in shard + allows override settings on each shard and replica separatelly + # nullable: true + properties: + shardsCount: + type: integer + description: | + how much shards for current ClickHouse cluster will run in Kubernetes, + each shard contains shared-nothing part of data and contains set of replicas, + cluster contains 1 shard by default" + replicasCount: + type: integer + description: | + how much replicas in each shards for current cluster will run in Kubernetes, + each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + every shard contains 1 replica by default" + shards: + type: array + description: | + optional, allows override top-level `chi.spec.configuration`, cluster-level + `chi.spec.configuration.clusters` settings for each shard separately, + use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + definitionType: + type: string + description: "DEPRECATED - to be removed soon" + weight: + type: integer + description: | + optional, 1 by default, allows setup shard <weight> setting which will use during insert into tables with `Distributed` engine, + will apply in <remote_servers> inside ConfigMap which will mount in /etc/clickhouse-server/config.d/chop-generated-remote_servers.xml + More details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + internalReplication: + <<: *TypeStringBool + description: | + optional, `true` by default when `chi.spec.configuration.clusters[].layout.ReplicaCount` > 1 and 0 otherwise + allows setup <internal_replication> setting which will use during insert into tables with `Distributed` engine for insert only in one live replica and other replicas will download inserted data during replication, + will apply in <remote_servers> inside ConfigMap which will mount in /etc/clickhouse-server/config.d/chop-generated-remote_servers.xml + More details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one shard during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` and cluster-level `chi.spec.configuration.clusters.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files` + templates: + <<: *TypeTemplateNames + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected shard + override top-level `chi.spec.configuration.templates` and cluster-level `chi.spec.configuration.clusters.templates` + replicasCount: + type: integer + description: | + optional, how much replicas in selected shard for selected ClickHouse cluster will run in Kubernetes, each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + shard contains 1 replica by default + override cluster-level `chi.spec.configuration.clusters.layout.replicasCount` + minimum: 1 + replicas: + type: array + description: | + optional, allows override behavior for selected replicas from cluster-level `chi.spec.configuration.clusters` and shard-level `chi.spec.configuration.clusters.layout.shards` + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + <<: *TypeStringBool + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + <<: *TypeStringBool + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `tcp` for selected replica, override `chi.spec.templates.hostTemplates.spec.tcpPort` + allows connect to `clickhouse-server` via TCP Native protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `http` for selected replica, override `chi.spec.templates.hostTemplates.spec.httpPort` + allows connect to `clickhouse-server` via HTTP protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `interserver` for selected replica, override `chi.spec.templates.hostTemplates.spec.interserverHTTPPort` + allows connect between replicas inside same shard during fetch replicated data parts HTTP protocol + minimum: 1 + maximum: 65535 + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and shard-level `chi.spec.configuration.clusters.layout.shards.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files`, cluster-level `chi.spec.configuration.clusters.files` and shard-level `chi.spec.configuration.clusters.layout.shards.files` + templates: + <<: *TypeTemplateNames + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` and shard-level `chi.spec.configuration.clusters.layout.shards.templates` + replicas: + type: array + description: "optional, allows override top-level `chi.spec.configuration` and cluster-level `chi.spec.configuration.clusters` configuration for each replica and each shard relates to selected replica, use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and will ignore if shard-level `chi.spec.configuration.clusters.layout.shards` present + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + <<: *TypeTemplateNames + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` + shardsCount: + type: integer + description: "optional, count of shards related to current replica, you can override each shard behavior on low-level `chi.spec.configuration.clusters.layout.replicas.shards`" + minimum: 1 + shards: + type: array + description: "optional, list of shards related to current replica, will ignore if `chi.spec.configuration.clusters.layout.shards` presents" + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + <<: *TypeStringBool + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + <<: *TypeStringBool + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `tcp` for selected shard, override `chi.spec.templates.hostTemplates.spec.tcpPort` + allows connect to `clickhouse-server` via TCP Native protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `http` for selected shard, override `chi.spec.templates.hostTemplates.spec.httpPort` + allows connect to `clickhouse-server` via HTTP protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `interserver` for selected shard, override `chi.spec.templates.hostTemplates.spec.interserverHTTPPort` + allows connect between replicas inside same shard during fetch replicated data parts HTTP protocol + minimum: 1 + maximum: 65535 + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and replica-level `chi.spec.configuration.clusters.layout.replicas.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + <<: *TypeTemplateNames + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates` + templates: + type: object + description: "allows define templates which will use for render Kubernetes resources like StatefulSet, ConfigMap, Service, PVC, by default, clickhouse-operator have own templates, but you can override it" + # nullable: true + properties: + hostTemplates: + type: array + description: "hostTemplate will use during apply to generate `clickhose-server` config files" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.hostTemplate`, cluster-level `chi.spec.configuration.clusters.templates.hostTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.hostTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.hostTemplate`" + type: string + portDistribution: + type: array + description: "define how will distribute numeric values of named ports in `Pod.spec.containers.ports` and clickhouse-server configs" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "type of distribution, when `Unspecified` (default value) then all listen ports on clickhouse-server configuration in all Pods will have the same value, when `ClusterScopeIndex` then ports will increment to offset from base value depends on shard and replica index inside cluster with combination of `chi.spec.templates.podTemlates.spec.HostNetwork` it allows setup ClickHouse cluster inside Kubernetes and provide access via external network bypass Kubernetes internal network" + enum: + # List PortDistributionXXX constants + - "" + - "Unspecified" + - "ClusterScopeIndex" + spec: + # Host + type: object + properties: + name: + type: string + description: "by default, hostname will generate, but this allows define custom name for each `clickhouse-server`" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + <<: *TypeStringBool + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + <<: *TypeStringBool + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `tcp_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=tcp]` + More info: https://clickhouse.tech/docs/en/interfaces/tcp/ + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `http_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=http]` + More info: https://clickhouse.tech/docs/en/interfaces/http/ + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `interserver_http_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=interserver]` + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#interserver-http-port + minimum: 1 + maximum: 65535 + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + templates: + <<: *TypeTemplateNames + description: "be careful, this part of CRD allows override template inside template, don't use it if you don't understand what you do" + + podTemplates: + type: array + description: | + podTemplate will use during render `Pod` inside `StatefulSet.spec` and allows define rendered `Pod.spec`, pod scheduling distribution and pod zone + More information: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatespodtemplates + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.podTemplate`, cluster-level `chi.spec.configuration.clusters.templates.podTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.podTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.podTemplate`" + generateName: + type: string + description: "allows define format for generated `Pod` name, look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates for details about available template variables" + zone: + type: object + description: "allows define custom zone name and will separate ClickHouse `Pods` between nodes, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + #required: + # - values + properties: + key: + type: string + description: "optional, if defined, allows select kubernetes nodes by label with `name` equal `key`" + values: + type: array + description: "optional, if defined, allows select kubernetes nodes by label with `value` in `values`" + # nullable: true + items: + type: string + distribution: + type: string + description: "DEPRECATED, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + enum: + - "" + - "Unspecified" + - "OnePerHost" + podDistribution: + type: array + description: "define ClickHouse Pod distribution policy between Kubernetes Nodes inside Shard, Replica, Namespace, CHI, another ClickHouse cluster" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "you can define multiple affinity policy types" + enum: + # List PodDistributionXXX constants + - "" + - "Unspecified" + - "ClickHouseAntiAffinity" + - "ShardAntiAffinity" + - "ReplicaAntiAffinity" + - "AnotherNamespaceAntiAffinity" + - "AnotherClickHouseInstallationAntiAffinity" + - "AnotherClusterAntiAffinity" + - "MaxNumberPerNode" + - "NamespaceAffinity" + - "ClickHouseInstallationAffinity" + - "ClusterAffinity" + - "ShardAffinity" + - "ReplicaAffinity" + - "PreviousTailAffinity" + - "CircularReplication" + scope: + type: string + description: "scope for apply each podDistribution" + enum: + # list PodDistributionScopeXXX constants + - "" + - "Unspecified" + - "Shard" + - "Replica" + - "Cluster" + - "ClickHouseInstallation" + - "Namespace" + number: + type: integer + description: "define, how much ClickHouse Pods could be inside selected scope with selected distribution type" + minimum: 0 + maximum: 65535 + topologyKey: + type: string + description: | + use for inter-pod affinity look to `pod.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution.podAffinityTerm.topologyKey`, + more info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity" + metadata: + type: object + description: | + allows pass standard object's metadata from template to Pod + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify PodSpec + type: object + description: "allows define whole Pod.spec inside StaefulSet.spec, look to https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates for details" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + + volumeClaimTemplates: + type: array + description: | + allows define template for rendering `PVC` kubernetes resource, which would use inside `Pod` for mount clickhouse `data`, clickhouse `logs` or something else + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + top-level `chi.spec.defaults.templates.dataVolumeClaimTemplate` or `chi.spec.defaults.templates.logVolumeClaimTemplate`, + cluster-level `chi.spec.configuration.clusters.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.templates.logVolumeClaimTemplate`, + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.shards.temlates.logVolumeClaimTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.replicas.templates.logVolumeClaimTemplate` + provisioner: *TypePVCProvisioner + reclaimPolicy: *TypePVCReclaimPolicy + metadata: + type: object + description: | + allows to pass standard object's metadata from template to PVC + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + description: | + allows define all aspects of `PVC` resource + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims + # nullable: true + x-kubernetes-preserve-unknown-fields: true + serviceTemplates: + type: array + description: | + allows define template for rendering `Service` which would get endpoint from Pods which scoped chi-wide, cluster-wide, shard-wide, replica-wide level + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + chi-level `chi.spec.defaults.templates.serviceTemplate` + cluster-level `chi.spec.configuration.clusters.templates.clusterServiceTemplate` + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.shardServiceTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.replicaServiceTemplate` or `chi.spec.configuration.clusters.layout.shards.replicas.replicaServiceTemplate` + generateName: + type: string + description: | + allows define format for generated `Service` name, + look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates + for details about available template variables" + metadata: + # TODO specify ObjectMeta + type: object + description: | + allows pass standard object's metadata from template to Service + Could be use for define specificly for Cloud Provider metadata which impact to behavior of service + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify ServiceSpec + type: object + description: | + describe behavior of generated Service + More info: https://kubernetes.io/docs/concepts/services-networking/service/ + # nullable: true + x-kubernetes-preserve-unknown-fields: true + useTemplates: + type: array + description: | + list of `ClickHouseInstallationTemplate` (chit) resource names which will merge with current `CHI` + manifest during render Kubernetes resources to create related ClickHouse clusters" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "name of `ClickHouseInstallationTemplate` (chit) resource" + namespace: + type: string + description: "Kubernetes namespace where need search `chit` resource, depending on `watchNamespaces` settings in `clickhouse-operator`" + useType: + type: string + description: "optional, current strategy is only merge, and current `chi` settings have more priority than merged template `chit`" + enum: + # List useTypeXXX constants from model + - "" + - "merge" +--- +# Template Parameters: +# +# NONE +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clickhouseoperatorconfigurations.clickhouse.altinity.com + labels: + clickhouse.altinity.com/chop: 0.26.3 +spec: + group: clickhouse.altinity.com + scope: Namespaced + names: + kind: ClickHouseOperatorConfiguration + singular: clickhouseoperatorconfiguration + plural: clickhouseoperatorconfigurations + shortNames: + - chopconf + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: namespaces + type: string + description: Watch namespaces + jsonPath: .status + - name: age + type: date + description: Age of the resource + # Displayed in all priorities + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + description: "allows customize `clickhouse-operator` settings, need restart clickhouse-operator pod after adding, more details https://github.com/Altinity/clickhouse-operator/blob/master/docs/operator_configuration.md" + x-kubernetes-preserve-unknown-fields: true + properties: + status: + type: object + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + description: | + Allows to define settings of the clickhouse-operator. + More info: https://github.com/Altinity/clickhouse-operator/blob/master/config/config.yaml + Check into etc-clickhouse-operator* ConfigMaps if you need more control + x-kubernetes-preserve-unknown-fields: true + properties: + watch: + type: object + description: "Parameters for watch kubernetes resources which used by clickhouse-operator deployment" + properties: + namespaces: + type: object + description: "List of namespaces where clickhouse-operator watches for events." + x-kubernetes-preserve-unknown-fields: true + clickhouse: + type: object + description: "Clickhouse related parameters used by clickhouse-operator" + properties: + configuration: + type: object + properties: + file: + type: object + properties: + path: + type: object + description: | + Each 'path' can be either absolute or relative. + In case path is absolute - it is used as is. + In case path is relative - it is relative to the folder where configuration file you are reading right now is located. + properties: + common: + type: string + description: | + Path to the folder where ClickHouse configuration files common for all instances within a CHI are located. + Default value - config.d + host: + type: string + description: | + Path to the folder where ClickHouse configuration files unique for each instance (host) within a CHI are located. + Default value - conf.d + user: + type: string + description: | + Path to the folder where ClickHouse configuration files with users settings are located. + Files are common for all instances within a CHI. + Default value - users.d + user: + type: object + description: "Default parameters for any user which will create" + properties: + default: + type: object + properties: + profile: + type: string + description: "ClickHouse server configuration `<profile>...</profile>` for any <user>" + quota: + type: string + description: "ClickHouse server configuration `<quota>...</quota>` for any <user>" + networksIP: + type: array + description: "ClickHouse server configuration `<networks><ip>...</ip></networks>` for any <user>" + items: + type: string + password: + type: string + description: "ClickHouse server configuration `<password>...</password>` for any <user>" + network: + type: object + description: "Default network parameters for any user which will create" + properties: + hostRegexpTemplate: + type: string + description: "ClickHouse server configuration `<host_regexp>...</host_regexp>` for any <user>" + configurationRestartPolicy: + type: object + description: "Configuration restart policy describes what configuration changes require ClickHouse restart" + properties: + rules: + type: array + description: "Array of set of rules per specified ClickHouse versions" + items: + type: object + properties: + version: + type: string + description: "ClickHouse version expression" + rules: + type: array + description: "Set of configuration rules for specified ClickHouse version" + items: + type: object + description: "setting: value pairs for configuration restart policy" + x-kubernetes-preserve-unknown-fields: true + access: + type: object + description: "parameters which use for connect to clickhouse from clickhouse-operator deployment" + properties: + scheme: + type: string + description: "The scheme to user for connecting to ClickHouse. Possible values: http, https, auto" + username: + type: string + description: "ClickHouse username to be used by operator to connect to ClickHouse instances, deprecated, use chCredentialsSecretName" + password: + type: string + description: "ClickHouse password to be used by operator to connect to ClickHouse instances, deprecated, use chCredentialsSecretName" + rootCA: + type: string + description: "Root certificate authority that clients use when verifying server certificates. Used for https connection to ClickHouse" + secret: + type: object + properties: + namespace: + type: string + description: "Location of k8s Secret with username and password to be used by operator to connect to ClickHouse instances" + name: + type: string + description: "Name of k8s Secret with username and password to be used by operator to connect to ClickHouse instances" + port: + type: integer + minimum: 1 + maximum: 65535 + description: "Port to be used by operator to connect to ClickHouse instances" + timeouts: + type: object + description: "Timeouts used to limit connection and queries from the operator to ClickHouse instances, In seconds" + properties: + connect: + type: integer + minimum: 1 + maximum: 10 + description: "Timout to setup connection from the operator to ClickHouse instances. In seconds." + query: + type: integer + minimum: 1 + maximum: 600 + description: "Timout to perform SQL query from the operator to ClickHouse instances. In seconds." + addons: + type: object + description: "Configuration addons specifies additional settings" + properties: + rules: + type: array + description: "Array of set of rules per specified ClickHouse versions" + items: + type: object + properties: + version: + type: string + description: "ClickHouse version expression" + spec: + type: object + description: "spec" + properties: + configuration: + type: object + description: "allows configure multiple aspects and behavior for `clickhouse-server` instance and also allows describe multiple `clickhouse-server` clusters inside one `chi` resource" + properties: + users: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + profiles: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + quotas: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + settings: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + files: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + metrics: + type: object + description: "parameters which use for connect to fetch metrics from clickhouse by clickhouse-operator" + properties: + timeouts: + type: object + description: | + Timeouts used to limit connection and queries from the metrics exporter to ClickHouse instances + Specified in seconds. + properties: + collect: + type: integer + minimum: 1 + maximum: 600 + description: | + Timeout used to limit metrics collection request. In seconds. + Upon reaching this timeout metrics collection is aborted and no more metrics are collected in this cycle. + All collected metrics are returned. + tablesRegexp: + type: string + description: | + Regexp to match tables in system database to fetch metrics from. + Multiple tables can be matched using regexp. Matched tables are merged using merge() table function. + Default is "^(metrics|custom_metrics)$". + template: + type: object + description: "Parameters which are used if you want to generate ClickHouseInstallationTemplate custom resources from files which are stored inside clickhouse-operator deployment" + properties: + chi: + type: object + properties: + policy: + type: string + description: | + CHI template updates handling policy + Possible policy values: + - ReadOnStart. Accept CHIT updates on the operators start only. + - ApplyOnNextReconcile. Accept CHIT updates at all time. Apply news CHITs on next regular reconcile of the CHI + enum: + - "" + - "ReadOnStart" + - "ApplyOnNextReconcile" + path: + type: string + description: "Path to folder where ClickHouseInstallationTemplate .yaml manifests are located." + reconcile: + type: object + description: "allow tuning reconciling process" + properties: + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileCHIsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "How many goroutines will be used to reconcile CHIs in parallel, 10 by default" + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "How many goroutines will be used to reconcile shards of a cluster in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + statefulSet: + type: object + description: "Allow change default behavior for reconciling StatefulSet which generated by clickhouse-operator" + properties: + create: + type: object + description: "Behavior during create StatefulSet" + properties: + onFailure: + type: string + description: | + What to do in case created StatefulSet is not in Ready after `statefulSetUpdateTimeout` seconds + Possible options: + 1. abort - do nothing, just break the process and wait for admin. + 2. delete - delete newly created problematic StatefulSet. + 3. ignore (default) - ignore error, pretend nothing happened and move on to the next StatefulSet. + update: + type: object + description: "Behavior during update StatefulSet" + properties: + timeout: + type: integer + description: "How many seconds to wait for created/updated StatefulSet to be Ready" + pollInterval: + type: integer + description: "How many seconds to wait between checks for created/updated StatefulSet status" + onFailure: + type: string + description: | + What to do in case updated StatefulSet is not in Ready after `statefulSetUpdateTimeout` seconds + Possible options: + 1. abort - do nothing, just break the process and wait for admin. + 2. rollback (default) - delete Pod and rollback StatefulSet to previous Generation. Pod would be recreated by StatefulSet based on rollback-ed configuration. + 3. ignore - ignore error, pretend nothing happened and move on to the next StatefulSet. + recreate: + type: object + description: "Behavior during recreate StatefulSet" + properties: + onDataLoss: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to PVC data loss or missing volumes. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate (default) - proceed and recreate StatefulSet. + onUpdateFailure: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to update failure or StatefulSet not ready. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate (default) - proceed and recreate StatefulSet. + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: &TypeStringBool + type: string + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be excluded from a ClickHouse cluster" + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + <<: *TypeStringBool + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + <<: *TypeStringBool + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + <<: *TypeStringBool + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + <<: *TypeStringBool + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + <<: *TypeStringBool + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + <<: *TypeStringBool + description: | + Whether the operator during host launch procedure should wait for readiness probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + <<: *TypeStringBool + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + <<: *TypeStringBool + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + <<: *TypeStringBool + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + annotation: + type: object + description: "defines which metadata.annotations items will include or exclude during render StatefulSet, Pod, PVC resources" + properties: + include: + type: array + description: | + When propagating labels from the chi's `metadata.annotations` section to child objects' `metadata.annotations`, + include annotations with names from the following list + items: + type: string + exclude: + type: array + description: | + When propagating labels from the chi's `metadata.annotations` section to child objects' `metadata.annotations`, + exclude annotations with names from the following list + items: + type: string + label: + type: object + description: "defines which metadata.labels will include or exclude during render StatefulSet, Pod, PVC resources" + properties: + include: + type: array + description: | + When propagating labels from the chi's `metadata.labels` section to child objects' `metadata.labels`, + include labels from the following list + items: + type: string + exclude: + type: array + items: + type: string + description: | + When propagating labels from the chi's `metadata.labels` section to child objects' `metadata.labels`, + exclude labels from the following list + appendScope: + <<: *TypeStringBool + description: | + Whether to append *Scope* labels to StatefulSet and Pod + - "LabelShardScopeIndex" + - "LabelReplicaScopeIndex" + - "LabelCHIScopeIndex" + - "LabelCHIScopeCycleSize" + - "LabelCHIScopeCycleIndex" + - "LabelCHIScopeCycleOffset" + - "LabelClusterScopeIndex" + - "LabelClusterScopeCycleSize" + - "LabelClusterScopeCycleIndex" + - "LabelClusterScopeCycleOffset" + metrics: + type: object + description: "defines metrics exporter options" + properties: + labels: + type: object + description: "defines metric labels options" + properties: + exclude: + type: array + description: | + When adding labels to a metric exclude labels with names from the following list + items: + type: string + status: + type: object + description: "defines status options" + properties: + fields: + type: object + description: "defines status fields options" + properties: + action: + <<: *TypeStringBool + description: "Whether the operator should fill status field 'action'" + actions: + <<: *TypeStringBool + description: "Whether the operator should fill status field 'actions'" + error: + <<: *TypeStringBool + description: "Whether the operator should fill status field 'error'" + errors: + <<: *TypeStringBool + description: "Whether the operator should fill status field 'errors'" + statefulSet: + type: object + description: "define StatefulSet-specific parameters" + properties: + revisionHistoryLimit: + type: integer + description: | + revisionHistoryLimit is the maximum number of revisions that will be + maintained in the StatefulSet's revision history. + Look details in `statefulset.spec.revisionHistoryLimit` + pod: + type: object + description: "define pod specific parameters" + properties: + terminationGracePeriod: + type: integer + description: | + Optional duration in seconds the pod needs to terminate gracefully. + Look details in `pod.spec.terminationGracePeriodSeconds` + logger: + type: object + description: "allow setup clickhouse-operator logger behavior" + properties: + logtostderr: + type: string + description: "boolean, allows logs to stderr" + alsologtostderr: + type: string + description: "boolean allows logs to stderr and files both" + v: + type: string + description: "verbosity level of clickhouse-operator log, default - 1 max - 9" + stderrthreshold: + type: string + vmodule: + type: string + description: | + Comma-separated list of filename=N, where filename (can be a pattern) must have no .go ext, and N is a V level. + Ex.: file*=2 sets the 'V' to 2 in all files with names like file*. + log_backtrace_at: + type: string + description: | + It can be set to a file and line number with a logging line. + Ex.: file.go:123 + Each time when this line is being executed, a stack trace will be written to the Info log. +--- +# Template Parameters: +# +# OPERATOR_VERSION=0.26.3 +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clickhousekeeperinstallations.clickhouse-keeper.altinity.com + labels: + clickhouse-keeper.altinity.com/chop: 0.26.3 +spec: + group: clickhouse-keeper.altinity.com + scope: Namespaced + names: + kind: ClickHouseKeeperInstallation + singular: clickhousekeeperinstallation + plural: clickhousekeeperinstallations + shortNames: + - chk + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: status + type: string + description: Resource status + jsonPath: .status.status + - name: version + type: string + description: Operator version + priority: 1 # show in wide view + jsonPath: .status.chop-version + - name: clusters + type: integer + description: Clusters count + jsonPath: .status.clusters + - name: shards + type: integer + description: Shards count + priority: 1 # show in wide view + jsonPath: .status.shards + - name: hosts + type: integer + description: Hosts count + jsonPath: .status.hosts + - name: taskID + type: string + description: TaskID + priority: 1 # show in wide view + jsonPath: .status.taskID + - name: hosts-completed + type: integer + description: Completed hosts count + jsonPath: .status.hostsCompleted + - name: hosts-updated + type: integer + description: Updated hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsUpdated + - name: hosts-added + type: integer + description: Added hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsAdded + - name: hosts-deleted + type: integer + description: Hosts deleted count + priority: 1 # show in wide view + jsonPath: .status.hostsDeleted + - name: endpoint + type: string + description: Client access endpoint + priority: 1 # show in wide view + jsonPath: .status.endpoint + - name: age + type: date + description: Age of the resource + # Displayed in all priorities + jsonPath: .metadata.creationTimestamp + - name: suspend + type: string + description: Suspend reconciliation + # Displayed in all priorities + jsonPath: .spec.suspend + subresources: + status: {} + schema: + openAPIV3Schema: + description: "define a set of Kubernetes resources (StatefulSet, PVC, Service, ConfigMap) which describe behavior one or more clusters" + type: object + required: + - spec + properties: + apiVersion: + description: | + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: | + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + type: object + description: | + Status contains many fields like a normalized configuration, clickhouse-operator version, current action and all applied action list, current taskID and all applied taskIDs and other + properties: + chop-version: + type: string + description: "Operator version" + chop-commit: + type: string + description: "Operator git commit SHA" + chop-date: + type: string + description: "Operator build date" + chop-ip: + type: string + description: "IP address of the operator's pod which managed this resource" + clusters: + type: integer + minimum: 0 + description: "Clusters count" + shards: + type: integer + minimum: 0 + description: "Shards count" + replicas: + type: integer + minimum: 0 + description: "Replicas count" + hosts: + type: integer + minimum: 0 + description: "Hosts count" + status: + type: string + description: "Status" + taskID: + type: string + description: "Current task id" + taskIDsStarted: + type: array + description: "Started task ids" + nullable: true + items: + type: string + taskIDsCompleted: + type: array + description: "Completed task ids" + nullable: true + items: + type: string + action: + type: string + description: "Action" + actions: + type: array + description: "Actions" + nullable: true + items: + type: string + error: + type: string + description: "Last error" + errors: + type: array + description: "Errors" + nullable: true + items: + type: string + hostsUnchanged: + type: integer + minimum: 0 + description: "Unchanged Hosts count" + hostsUpdated: + type: integer + minimum: 0 + description: "Updated Hosts count" + hostsAdded: + type: integer + minimum: 0 + description: "Added Hosts count" + hostsCompleted: + type: integer + minimum: 0 + description: "Completed Hosts count" + hostsDeleted: + type: integer + minimum: 0 + description: "Deleted Hosts count" + hostsDelete: + type: integer + minimum: 0 + description: "About to delete Hosts count" + pods: + type: array + description: "Pods" + nullable: true + items: + type: string + pod-ips: + type: array + description: "Pod IPs" + nullable: true + items: + type: string + fqdns: + type: array + description: "Pods FQDNs" + nullable: true + items: + type: string + endpoint: + type: string + description: "Endpoint" + endpoints: + type: array + description: "All endpoints" + nullable: true + items: + type: string + generation: + type: integer + minimum: 0 + description: "Generation" + normalized: + type: object + description: "Normalized resource requested" + nullable: true + x-kubernetes-preserve-unknown-fields: true + normalizedCompleted: + type: object + description: "Normalized resource completed" + nullable: true + x-kubernetes-preserve-unknown-fields: true + hostsWithTablesCreated: + type: array + description: "List of hosts with tables created by the operator" + nullable: true + items: + type: string + hostsWithReplicaCaughtUp: + type: array + description: "List of hosts with replica caught up" + nullable: true + items: + type: string + usedTemplates: + type: array + description: "List of templates used to build this CHI" + nullable: true + x-kubernetes-preserve-unknown-fields: true + items: + type: object + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + # x-kubernetes-preserve-unknown-fields: true + description: | + Specification of the desired behavior of one or more ClickHouse clusters + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md + properties: + taskID: + type: string + description: | + Allows to define custom taskID for CHI update and watch status of this update execution. + Displayed in all .status.taskID* fields. + By default (if not filled) every update of CHI manifest will generate random taskID + stop: &TypeStringBool + type: string + description: | + Allows to stop all ClickHouse Keeper clusters defined in a CHK. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + suspend: + <<: *TypeStringBool + description: | + Suspend reconciliation of resources managed by a ClickHouse Keeper. + Works as the following: + - When `suspend` is `true` operator stops reconciling all resources. + - When `suspend` is `false` or not set, operator reconciles all resources. + namespaceDomainPattern: + type: string + description: | + Custom domain pattern which will be used for DNS names of `Service` or `Pod`. + Typical use scenario - custom cluster domain in Kubernetes cluster + Example: %s.svc.my.test + reconciling: + type: object + description: "Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + # nullable: true + properties: + policy: + type: string + description: | + DISCUSSED TO BE DEPRECATED + Syntax sugar + Overrides all three 'reconcile.host.wait.{exclude, queries, include}' values from the operator's config + Possible values: + - wait - should wait to exclude host, complete queries and include host back into the cluster + - nowait - should NOT wait to exclude host, complete queries and include host back into the cluster + enum: + - "" + - "wait" + - "nowait" + configMapPropagationTimeout: + type: integer + description: | + Timeout in seconds for `clickhouse-operator` to wait for modified `ConfigMap` to propagate into the `Pod` + More details: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically + minimum: 0 + maximum: 3600 + cleanup: + type: object + description: "Optional, defines behavior for cleanup Kubernetes resources during reconcile cycle" + # nullable: true + properties: + unknownObjects: + type: object + description: | + Describes what clickhouse-operator should do with found Kubernetes resources which should be managed by clickhouse-operator, + but do not have `ownerReference` to any currently managed `ClickHouseInstallation` resource. + Default behavior is `Delete`" + # nullable: true + properties: + statefulSet: &TypeObjectsCleanup + type: string + description: "Behavior policy for unknown StatefulSet, `Delete` by default" + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + pvc: + type: string + <<: *TypeObjectsCleanup + description: "Behavior policy for unknown PVC, `Delete` by default" + configMap: + <<: *TypeObjectsCleanup + description: "Behavior policy for unknown ConfigMap, `Delete` by default" + service: + <<: *TypeObjectsCleanup + description: "Behavior policy for unknown Service, `Delete` by default" + reconcileFailedObjects: + type: object + description: | + Describes what clickhouse-operator should do with Kubernetes resources which are failed during reconcile. + Default behavior is `Retain`" + # nullable: true + properties: + statefulSet: + <<: *TypeObjectsCleanup + description: "Behavior policy for failed StatefulSet, `Retain` by default" + pvc: + <<: *TypeObjectsCleanup + description: "Behavior policy for failed PVC, `Retain` by default" + configMap: + <<: *TypeObjectsCleanup + description: "Behavior policy for failed ConfigMap, `Retain` by default" + service: + <<: *TypeObjectsCleanup + description: "Behavior policy for failed Service, `Retain` by default" + defaults: + type: object + description: | + define default behavior for whole ClickHouseInstallation, some behavior can be re-define on cluster, shard and replica level + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specdefaults + # nullable: true + properties: + replicasUseFQDN: + <<: *TypeStringBool + description: | + define should replicas be specified by FQDN in `<host></host>`. + In case of "no" will use short hostname and clickhouse-server will use kubernetes default suffixes for DNS lookup + "no" by default + distributedDDL: + type: object + description: | + allows change `<yandex><distributed_ddl></distributed_ddl></yandex>` settings + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings-distributed_ddl + # nullable: true + properties: + profile: + type: string + description: "Settings from this profile will be used to execute DDL queries" + storageManagement: + type: object + description: default storage management options + properties: + provisioner: &TypePVCProvisioner + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: &TypePVCReclaimPolicy + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + templates: &TypeTemplateNames + type: object + description: "optional, configuration of the templates names which will use for generate Kubernetes resources according to one or more ClickHouse clusters described in current ClickHouseInstallation (chi) resource" + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + configuration: + type: object + description: "allows configure multiple aspects and behavior for `clickhouse-server` instance and also allows describe multiple `clickhouse-server` clusters inside one `chi` resource" + # nullable: true + properties: + settings: &TypeSettings + type: object + description: | + allows configure multiple aspects and behavior for `clickhouse-keeper` instance + # nullable: true + x-kubernetes-preserve-unknown-fields: true + files: &TypeFiles + type: object + description: | + allows define content of any setting + + # nullable: true + x-kubernetes-preserve-unknown-fields: true + clusters: + type: array + description: | + describes clusters layout and allows change settings on cluster-level and replica-level + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "cluster name, used to identify set of servers and wide used during generate names of related Kubernetes resources" + minLength: 1 + # See namePartClusterMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` on current cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` + templates: + <<: *TypeTemplateNames + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected cluster + override top-level `chi.spec.configuration.templates` + pdbManaged: + <<: *TypeStringBool + description: | + Specifies whether the Pod Disruption Budget (PDB) should be managed. + During the next installation, if PDB management is enabled, the operator will + attempt to retrieve any existing PDB. If none is found, it will create a new one + and initiate a reconciliation loop. If PDB management is disabled, the existing PDB + will remain intact, and the reconciliation loop will not be executed. By default, + PDB management is enabled. + pdbMaxUnavailable: + type: integer + description: | + Pod eviction is allowed if at most "pdbMaxUnavailable" pods are unavailable after the eviction, + i.e. even in absence of the evicted pod. For example, one can prevent all voluntary evictions + by specifying 0. This is a mutually exclusive setting with "minAvailable". + minimum: 0 + maximum: 65535 + layout: + type: object + description: | + describe current cluster layout, how much shards in cluster, how much replica in shard + allows override settings on each shard and replica separatelly + # nullable: true + properties: + replicasCount: + type: integer + description: | + how much replicas in each shards for current cluster will run in Kubernetes, + each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + every shard contains 1 replica by default" + replicas: + type: array + description: "optional, allows override top-level `chi.spec.configuration` and cluster-level `chi.spec.configuration.clusters` configuration for each replica and each shard relates to selected replica, use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and will ignore if shard-level `chi.spec.configuration.clusters.layout.shards` present + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + <<: *TypeTemplateNames + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` + shardsCount: + type: integer + description: "optional, count of shards related to current replica, you can override each shard behavior on low-level `chi.spec.configuration.clusters.layout.replicas.shards`" + minimum: 1 + shards: + type: array + description: "optional, list of shards related to current replica, will ignore if `chi.spec.configuration.clusters.layout.shards` presents" + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + zkPort: + type: integer + minimum: 1 + maximum: 65535 + raftPort: + type: integer + minimum: 1 + maximum: 65535 + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and replica-level `chi.spec.configuration.clusters.layout.replicas.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + <<: *TypeTemplateNames + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates` + templates: + type: object + description: "allows define templates which will use for render Kubernetes resources like StatefulSet, ConfigMap, Service, PVC, by default, clickhouse-operator have own templates, but you can override it" + # nullable: true + properties: + hostTemplates: + type: array + description: "hostTemplate will use during apply to generate `clickhose-server` config files" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.hostTemplate`, cluster-level `chi.spec.configuration.clusters.templates.hostTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.hostTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.hostTemplate`" + type: string + portDistribution: + type: array + description: "define how will distribute numeric values of named ports in `Pod.spec.containers.ports` and clickhouse-server configs" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "type of distribution, when `Unspecified` (default value) then all listen ports on clickhouse-server configuration in all Pods will have the same value, when `ClusterScopeIndex` then ports will increment to offset from base value depends on shard and replica index inside cluster with combination of `chi.spec.templates.podTemlates.spec.HostNetwork` it allows setup ClickHouse cluster inside Kubernetes and provide access via external network bypass Kubernetes internal network" + enum: + # List PortDistributionXXX constants + - "" + - "Unspecified" + - "ClusterScopeIndex" + spec: + # Host + type: object + properties: + name: + type: string + description: "by default, hostname will generate, but this allows define custom name for each `clickhouse-server`" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + zkPort: + type: integer + minimum: 1 + maximum: 65535 + raftPort: + type: integer + minimum: 1 + maximum: 65535 + settings: + <<: *TypeSettings + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + <<: *TypeFiles + description: | + optional, allows define content of any setting file inside each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + templates: + <<: *TypeTemplateNames + description: "be careful, this part of CRD allows override template inside template, don't use it if you don't understand what you do" + + podTemplates: + type: array + description: | + podTemplate will use during render `Pod` inside `StatefulSet.spec` and allows define rendered `Pod.spec`, pod scheduling distribution and pod zone + More information: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatespodtemplates + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.podTemplate`, cluster-level `chi.spec.configuration.clusters.templates.podTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.podTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.podTemplate`" + generateName: + type: string + description: "allows define format for generated `Pod` name, look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates for details about available template variables" + zone: + type: object + description: "allows define custom zone name and will separate ClickHouse `Pods` between nodes, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + #required: + # - values + properties: + key: + type: string + description: "optional, if defined, allows select kubernetes nodes by label with `name` equal `key`" + values: + type: array + description: "optional, if defined, allows select kubernetes nodes by label with `value` in `values`" + # nullable: true + items: + type: string + distribution: + type: string + description: "DEPRECATED, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + enum: + - "" + - "Unspecified" + - "OnePerHost" + podDistribution: + type: array + description: "define ClickHouse Pod distribution policy between Kubernetes Nodes inside Shard, Replica, Namespace, CHI, another ClickHouse cluster" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "you can define multiple affinity policy types" + enum: + # List PodDistributionXXX constants + - "" + - "Unspecified" + - "ClickHouseAntiAffinity" + - "ShardAntiAffinity" + - "ReplicaAntiAffinity" + - "AnotherNamespaceAntiAffinity" + - "AnotherClickHouseInstallationAntiAffinity" + - "AnotherClusterAntiAffinity" + - "MaxNumberPerNode" + - "NamespaceAffinity" + - "ClickHouseInstallationAffinity" + - "ClusterAffinity" + - "ShardAffinity" + - "ReplicaAffinity" + - "PreviousTailAffinity" + - "CircularReplication" + scope: + type: string + description: "scope for apply each podDistribution" + enum: + # list PodDistributionScopeXXX constants + - "" + - "Unspecified" + - "Shard" + - "Replica" + - "Cluster" + - "ClickHouseInstallation" + - "Namespace" + number: + type: integer + description: "define, how much ClickHouse Pods could be inside selected scope with selected distribution type" + minimum: 0 + maximum: 65535 + topologyKey: + type: string + description: | + use for inter-pod affinity look to `pod.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution.podAffinityTerm.topologyKey`, + more info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity" + metadata: + type: object + description: | + allows pass standard object's metadata from template to Pod + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify PodSpec + type: object + description: "allows define whole Pod.spec inside StaefulSet.spec, look to https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates for details" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + + volumeClaimTemplates: + type: array + description: | + allows define template for rendering `PVC` kubernetes resource, which would use inside `Pod` for mount clickhouse `data`, clickhouse `logs` or something else + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + top-level `chi.spec.defaults.templates.dataVolumeClaimTemplate` or `chi.spec.defaults.templates.logVolumeClaimTemplate`, + cluster-level `chi.spec.configuration.clusters.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.templates.logVolumeClaimTemplate`, + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.shards.temlates.logVolumeClaimTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.replicas.templates.logVolumeClaimTemplate` + provisioner: *TypePVCProvisioner + reclaimPolicy: *TypePVCReclaimPolicy + metadata: + type: object + description: | + allows to pass standard object's metadata from template to PVC + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + description: | + allows define all aspects of `PVC` resource + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims + # nullable: true + x-kubernetes-preserve-unknown-fields: true + serviceTemplates: + type: array + description: | + allows define template for rendering `Service` which would get endpoint from Pods which scoped chi-wide, cluster-wide, shard-wide, replica-wide level + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + chi-level `chi.spec.defaults.templates.serviceTemplate` + cluster-level `chi.spec.configuration.clusters.templates.clusterServiceTemplate` + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.shardServiceTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.replicaServiceTemplate` or `chi.spec.configuration.clusters.layout.shards.replicas.replicaServiceTemplate` + generateName: + type: string + description: | + allows define format for generated `Service` name, + look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates + for details about available template variables" + metadata: + # TODO specify ObjectMeta + type: object + description: | + allows pass standard object's metadata from template to Service + Could be use for define specificly for Cloud Provider metadata which impact to behavior of service + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify ServiceSpec + type: object + description: | + describe behavior of generated Service + More info: https://kubernetes.io/docs/concepts/services-networking/service/ + # nullable: true + x-kubernetes-preserve-unknown-fields: true +--- +# Template Parameters: +# +# COMMENT= +# NAMESPACE=kube-system +# NAME=clickhouse-operator +# +# Setup ServiceAccount +apiVersion: v1 +kind: ServiceAccount +metadata: + name: clickhouse-operator + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 +--- +# Template Parameters: +# +# NAMESPACE=kube-system +# COMMENT=# +# ROLE_KIND=ClusterRole +# ROLE_NAME=clickhouse-operator-kube-system +# ROLE_BINDING_KIND=ClusterRoleBinding +# ROLE_BINDING_NAME=clickhouse-operator-kube-system +# + + +# Specifies either +# ClusterRole +# or +# Role +# to be bound to ServiceAccount. +# ClusterRole is namespace-less and must have unique name +# Role is namespace-bound +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: clickhouse-operator-kube-system + #namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 +rules: + + # + # Core API group + # + + - apiGroups: + - "" + resources: + - configmaps + - services + - persistentvolumeclaims + - secrets + verbs: + - get + - list + - patch + - update + - watch + - create + - delete + - apiGroups: + - "" + resources: + - endpoints + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - patch + - update + - watch + - delete + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + + # + # apps.* resources + # + + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - get + - list + - patch + - update + - watch + - create + - delete + - apiGroups: + - apps + resources: + - replicasets + verbs: + - get + - patch + - update + - delete + # The operator deployment personally, identified by name + - apiGroups: + - apps + resources: + - deployments + resourceNames: + - clickhouse-operator + verbs: + - get + - patch + - update + - delete + + # + # policy.* resources + # + + - apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - get + - list + - patch + - update + - watch + - create + - delete + + # + # discovery.* resources + # + + - apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - list + - watch + + # + # apiextensions + # + + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - list + + # + # The operator's specific Custom Resources + # + + # clickhouse - related resources + - apiGroups: + - clickhouse.altinity.com + resources: + - clickhouseinstallations + verbs: + - get + - list + - watch + - patch + - update + - delete + - apiGroups: + - clickhouse.altinity.com + resources: + - clickhouseinstallationtemplates + - clickhouseoperatorconfigurations + verbs: + - get + - list + - watch + - apiGroups: + - clickhouse.altinity.com + resources: + - clickhouseinstallations/finalizers + - clickhouseinstallationtemplates/finalizers + - clickhouseoperatorconfigurations/finalizers + verbs: + - update + - apiGroups: + - clickhouse.altinity.com + resources: + - clickhouseinstallations/status + - clickhouseinstallationtemplates/status + - clickhouseoperatorconfigurations/status + verbs: + - get + - update + - patch + - create + - delete + + # clickhouse-keeper - related resources + - apiGroups: + - clickhouse-keeper.altinity.com + resources: + - clickhousekeeperinstallations + verbs: + - get + - list + - watch + - patch + - update + - delete + - apiGroups: + - clickhouse-keeper.altinity.com + resources: + - clickhousekeeperinstallations/finalizers + verbs: + - update + - apiGroups: + - clickhouse-keeper.altinity.com + resources: + - clickhousekeeperinstallations/status + verbs: + - get + - update + - patch + - create + - delete +--- +# Specifies either +# ClusterRoleBinding between ClusterRole and ServiceAccount. +# or +# RoleBinding between Role and ServiceAccount. +# ClusterRoleBinding is namespace-less and must have unique name +# RoleBinding is namespace-bound +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: clickhouse-operator-kube-system + #namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: clickhouse-operator-kube-system +subjects: + - kind: ServiceAccount + name: clickhouse-operator + namespace: kube-system +--- +# Template Parameters: +# +# NAMESPACE=kube-system +# COMMENT= +# ROLE_KIND=Role +# ROLE_NAME=clickhouse-operator +# ROLE_BINDING_KIND=RoleBinding +# ROLE_BINDING_NAME=clickhouse-operator +# + + +# Specifies either +# ClusterRole +# or +# Role +# to be bound to ServiceAccount. +# ClusterRole is namespace-less and must have unique name +# Role is namespace-bound +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: clickhouse-operator + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 +rules: + + # + # Core API group + # + + - apiGroups: + - "" + resources: + - configmaps + - services + - persistentvolumeclaims + - secrets + verbs: + - get + - list + - patch + - update + - watch + - create + - delete + - apiGroups: + - "" + resources: + - endpoints + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - patch + - update + - watch + - delete + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + + # + # apps.* resources + # + + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - get + - list + - patch + - update + - watch + - create + - delete + - apiGroups: + - apps + resources: + - replicasets + verbs: + - get + - patch + - update + - delete + # The operator deployment personally, identified by name + - apiGroups: + - apps + resources: + - deployments + resourceNames: + - clickhouse-operator + verbs: + - get + - patch + - update + - delete + + # + # policy.* resources + # + + - apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - get + - list + - patch + - update + - watch + - create + - delete + + # + # discovery.* resources + # + + - apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - list + - watch + + # + # apiextensions + # + + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - list + + # + # The operator's specific Custom Resources + # + + # clickhouse - related resources + - apiGroups: + - clickhouse.altinity.com + resources: + - clickhouseinstallations + verbs: + - get + - list + - watch + - patch + - update + - delete + - apiGroups: + - clickhouse.altinity.com + resources: + - clickhouseinstallationtemplates + - clickhouseoperatorconfigurations + verbs: + - get + - list + - watch + - apiGroups: + - clickhouse.altinity.com + resources: + - clickhouseinstallations/finalizers + - clickhouseinstallationtemplates/finalizers + - clickhouseoperatorconfigurations/finalizers + verbs: + - update + - apiGroups: + - clickhouse.altinity.com + resources: + - clickhouseinstallations/status + - clickhouseinstallationtemplates/status + - clickhouseoperatorconfigurations/status + verbs: + - get + - update + - patch + - create + - delete + + # clickhouse-keeper - related resources + - apiGroups: + - clickhouse-keeper.altinity.com + resources: + - clickhousekeeperinstallations + verbs: + - get + - list + - watch + - patch + - update + - delete + - apiGroups: + - clickhouse-keeper.altinity.com + resources: + - clickhousekeeperinstallations/finalizers + verbs: + - update + - apiGroups: + - clickhouse-keeper.altinity.com + resources: + - clickhousekeeperinstallations/status + verbs: + - get + - update + - patch + - create + - delete +--- +# Specifies either +# ClusterRoleBinding between ClusterRole and ServiceAccount. +# or +# RoleBinding between Role and ServiceAccount. +# ClusterRoleBinding is namespace-less and must have unique name +# RoleBinding is namespace-bound +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: clickhouse-operator + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: clickhouse-operator +subjects: + - kind: ServiceAccount + name: clickhouse-operator + namespace: kube-system +--- +# Template Parameters: +# +# NAME=etc-clickhouse-operator-files +# NAMESPACE=kube-system +# COMMENT= +# +apiVersion: v1 +kind: ConfigMap +metadata: + name: etc-clickhouse-operator-files + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 + app: clickhouse-operator +data: + config.yaml: | + # IMPORTANT + # This file is auto-generated + # Do not edit this file - all changes would be lost + # Edit appropriate template in the following folder: + # deploy/builder/templates-config + # IMPORTANT + # + # Template parameters available: + # WATCH_NAMESPACES= + # CH_USERNAME_PLAIN= + # CH_PASSWORD_PLAIN= + # CH_CREDENTIALS_SECRET_NAMESPACE= + # CH_CREDENTIALS_SECRET_NAME=clickhouse-operator + # VERBOSITY=1 + + ################################################ + ## + ## Watch section + ## + ################################################ + watch: + # List of namespaces where clickhouse-operator watches for events. + # Concurrently running operators should watch on different namespaces. + # IMPORTANT + # Regexp is applicable. + namespaces: [] + + clickhouse: + configuration: + ################################################ + ## + ## Configuration files section + ## + ################################################ + file: + # Each 'path' can be either absolute or relative. + # In case path is absolute - it is used as is + # In case path is relative - it is relative to the folder where configuration file you are reading right now is located. + path: + # Path to the folder where ClickHouse configuration files common for all instances within a CHI are located. + common: chi/config.d + # Path to the folder where ClickHouse configuration files unique for each instance (host) within a CHI are located. + host: chi/conf.d + # Path to the folder where ClickHouse configuration files with users' settings are located. + # Files are common for all instances within a CHI. + user: chi/users.d + ################################################ + ## + ## Configuration users section + ## + ################################################ + user: + # Default settings for user accounts, created by the operator. + # IMPORTANT. These are not access credentials or settings for 'default' user account, + # it is a template for filling out missing fields for all user accounts to be created by the operator, + # with the following EXCEPTIONS: + # 1. 'default' user account DOES NOT use provided password, but uses all the rest of the fields. + # Password for 'default' user account has to be provided explicitly, if to be used. + # 2. CHOP user account DOES NOT use: + # - profile setting. It uses predefined profile called 'clickhouse_operator' + # - quota setting. It uses empty quota name. + # - networks IP setting. Operator specifies 'networks/ip' user setting to match operators' pod IP only. + # - password setting. Password for CHOP account is used from 'clickhouse.access.*' section + default: + # Default values for ClickHouse user account(s) created by the operator + # 1. user/profile - string + # 2. user/quota - string + # 3. user/networks/ip - multiple strings + # 4. user/password - string + # These values can be overwritten on per-user basis. + profile: "default" + quota: "default" + networksIP: + - "::1" + - "127.0.0.1" + password: "default" + ################################################ + ## + ## Configuration network section + ## + ################################################ + network: + # Default host_regexp to limit network connectivity from outside + hostRegexpTemplate: "(chi-{chi}-[^.]+\\d+-\\d+|clickhouse\\-{chi})\\.{namespace}\\.svc\\.cluster\\.local$" + + ################################################ + ## + ## Configuration restart policy section + ## Configuration restart policy describes what configuration changes require ClickHouse restart + ## + ################################################ + configurationRestartPolicy: + rules: + # IMPORTANT! + # Special version of "*" - default version - has to satisfy all ClickHouse versions. + # Default version will also be used in case ClickHouse version is unknown. + # ClickHouse version may be unknown due to host being down - for example, because of incorrect "settings" section. + # ClickHouse is not willing to start in case incorrect/unknown settings are provided in config file. + - version: "*" + rules: + # see https://kb.altinity.com/altinity-kb-setup-and-maintenance/altinity-kb-server-config-files/#server-config-configxml-sections-which-dont-require-restart + # to be replaced with "select * from system.server_settings where changeable_without_restart = 'No'" + + - settings/*: "yes" + + # single values + - settings/access_control_path: "no" + - settings/dictionaries_config: "no" + - settings/max_server_memory_*: "no" + - settings/max_*_to_drop: "no" + - settings/max_concurrent_queries: "no" + - settings/models_config: "no" + - settings/user_defined_executable_functions_config: "no" + + # structured XML + - settings/logger/*: "no" + - settings/macros/*: "no" + - settings/remote_servers/*: "no" + - settings/user_directories/*: "no" + + # these settings should not lead to pod restarts + - settings/display_secrets_in_show_and_select: "no" + + - zookeeper/*: "no" + + - files/*.xml: "yes" + - files/config.d/*.xml: "yes" + - files/config.d/*dict*.xml: "no" + - files/config.d/*no_restart*: "no" + + # exceptions in default profile + - profiles/default/background_*_pool_size: "yes" + - profiles/default/max_*_for_server: "yes" + - version: "21.*" + rules: + - settings/logger: "yes" + + ################################################# + ## + ## Access to ClickHouse instances + ## + ################################################ + access: + # Possible values for 'scheme' are: + # 1. http - force http to be used to connect to ClickHouse instances + # 2. https - force https to be used to connect to ClickHouse instances + # 3. auto - either http or https is selected based on open ports + scheme: "auto" + # ClickHouse credentials (username, password and port) to be used by the operator to connect to ClickHouse instances. + # These credentials are used for: + # 1. Metrics requests + # 2. Schema maintenance + # User with these credentials can be specified in additional ClickHouse .xml config files, + # located in 'clickhouse.configuration.file.path.user' folder + username: "" + password: "" + rootCA: "" + + # Location of the k8s Secret with username and password to be used by the operator to connect to ClickHouse instances. + # Can be used instead of explicitly specified username and password available in sections: + # - clickhouse.access.username + # - clickhouse.access.password + # Secret should have two keys: + # 1. username + # 2. password + secret: + # Empty `namespace` means that k8s secret would be looked in the same namespace where operator's pod is running. + namespace: "" + # Empty `name` means no k8s Secret would be looked for + name: "clickhouse-operator" + # Port where to connect to ClickHouse instances to + port: 8123 + + # Timeouts used to limit connection and queries from the operator to ClickHouse instances + # Specified in seconds. + timeouts: + # Timout to setup connection from the operator to ClickHouse instances. In seconds. + connect: 5 + # Timout to perform SQL query from the operator to ClickHouse instances. In seconds. + query: 4 + + ################################################ + ## + ## Addons specifies additional configuration sections + ## Should it be called something like "templates"? + ## + ################################################ + addons: + rules: + - version: "*" + spec: + configuration: + users: + profiles: + quotas: + settings: + files: + - version: ">= 23.3" + spec: + configuration: + ### + ### users.d is global while description depends on CH version which may vary on per-host basis + ### In case of global-ness this may be better to implement via auto-templates + ### + ### As a solution, this may be applied on the whole cluster based on any of its hosts + ### + ### What to do when host is just created? CH version is not known prior to CH started and user config is required before CH started. + ### We do not have any info about the cluster on initial creation + ### + users: + "{clickhouseOperatorUser}/access_management": 1 + "{clickhouseOperatorUser}/named_collection_control": 1 + "{clickhouseOperatorUser}/show_named_collections": 1 + "{clickhouseOperatorUser}/show_named_collections_secrets": 1 + profiles: + quotas: + settings: + files: + - version: ">= 23.5" + spec: + configuration: + users: + profiles: + clickhouse_operator/format_display_secrets_in_show_and_select: 1 + quotas: + settings: + ## + ## this may be added on per-host basis into host's conf.d folder + ## + display_secrets_in_show_and_select: 1 + files: + + ################################################# + ## + ## Metrics collection + ## + ################################################ + + metrics: + # Timeouts used to limit connection and queries from the metrics exporter to ClickHouse instances + # Specified in seconds. + timeouts: + # Timeout used to limit metrics collection request. In seconds. + # Upon reaching this timeout metrics collection is aborted and no more metrics are collected in this cycle. + # All collected metrics are returned. + collect: 9 + # Regexp to match tables in system database to fetch metrics from. + # Multiple tables can be matched using regexp. Matched tables are merged using merge() table function. + # Default is "^(metrics|custom_metrics)$" which fetches from both system.metrics and system.custom_metrics. + tablesRegexp: "^(metrics|custom_metrics)$" + + keeper: + configuration: + ################################################ + ## + ## Configuration files section + ## + ################################################ + file: + # Each 'path' can be either absolute or relative. + # In case path is absolute - it is used as is + # In case path is relative - it is relative to the folder where configuration file you are reading right now is located. + path: + # Path to the folder where Keeper configuration files common for all instances within a CHK are located. + common: chk/keeper_config.d + # Path to the folder where Keeper configuration files unique for each instance (host) within a CHK are located. + host: chk/conf.d + # Path to the folder where Keeper configuration files with users' settings are located. + # Files are common for all instances within a CHI. + user: chk/users.d + + ################################################ + ## + ## Template(s) management section + ## + ################################################ + template: + chi: + # CHI template updates handling policy + # Possible policy values: + # - ReadOnStart. Accept CHIT updates on the operator's start only. + # - ApplyOnNextReconcile. Accept CHIT updates at all time. Apply new CHITs on next regular reconcile of the CHI + policy: ApplyOnNextReconcile + + # Path to the folder where ClickHouseInstallation templates .yaml manifests are located. + # Templates are added to the list of all templates and used when CHI is reconciled. + # Templates are applied in sorted alpha-numeric order. + path: chi/templates.d + chk: + # CHK template updates handling policy + # Possible policy values: + # - ReadOnStart. Accept CHIT updates on the operators start only. + # - ApplyOnNextReconcile. Accept CHIT updates at all time. Apply new CHITs on next regular reconcile of the CHI + policy: ApplyOnNextReconcile + + # Path to the folder where ClickHouseInstallation templates .yaml manifests are located. + # Templates are added to the list of all templates and used when CHI is reconciled. + # Templates are applied in sorted alpha-numeric order. + path: chk/templates.d + + ################################################ + ## + ## Reconcile section + ## + ################################################ + reconcile: + # Reconcile runtime settings + runtime: + # Max number of concurrent CHI reconciles in progress + reconcileCHIsThreadsNumber: 10 + + # The operator reconciles shards concurrently in each CHI with the following limitations: + # 1. Number of shards being reconciled (and thus having hosts down) in each CHI concurrently + # can not be greater than 'reconcileShardsThreadsNumber'. + # 2. Percentage of shards being reconciled (and thus having hosts down) in each CHI concurrently + # can not be greater than 'reconcileShardsMaxConcurrencyPercent'. + # 3. The first shard is always reconciled alone. Concurrency starts from the second shard and onward. + # Thus limiting number of shards being reconciled (and thus having hosts down) in each CHI by both number and percentage + + # Max number of concurrent shard reconciles within one cluster in progress + reconcileShardsThreadsNumber: 5 + # Max percentage of concurrent shard reconciles within one cluster in progress + reconcileShardsMaxConcurrencyPercent: 50 + + # Reconcile StatefulSet scenario + statefulSet: + # Create StatefulSet scenario + create: + # What to do in case created StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds + # Possible options: + # 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is, + # do not try to fix or delete or update it, just abort reconcile cycle. + # Do not proceed to the next StatefulSet(s) and wait for an admin to assist. + # 2. delete - delete newly created problematic StatefulSet and follow 'abort' path afterwards. + # 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + onFailure: ignore + + # Update StatefulSet scenario + update: + # How many seconds to wait for created/updated StatefulSet to be 'Ready' + timeout: 300 + # How many seconds to wait between checks/polls for created/updated StatefulSet status + pollInterval: 5 + # What to do in case updated StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds + # Possible options: + # 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is, + # do not try to fix or delete or update it, just abort reconcile cycle. + # Do not proceed to the next StatefulSet(s) and wait for an admin to assist. + # 2. rollback - delete Pod and rollback StatefulSet to previous Generation. + # Pod would be recreated by StatefulSet based on rollback-ed StatefulSet configuration. + # Follow 'abort' path afterwards. + # 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + onFailure: abort + + # Recreate StatefulSet scenario + recreate: + # What to do in case operator is in need to recreate StatefulSet? + # Possible options: + # 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is, + # do not try to fix or delete or update it, just abort reconcile cycle. + # Do not proceed to the next StatefulSet(s) and wait for an admin to assist. + # 2. recreate - proceed and recreate StatefulSet. + + # Triggered when PVC data loss or missing volumes are detected + onDataLoss: recreate + # Triggered when StatefulSet update fails or StatefulSet is not ready + onUpdateFailure: recreate + + # Reconcile Host scenario + host: + # The operator during reconcile procedure should wait for a ClickHouse host to achieve the following conditions: + wait: + # Whether the operator during reconcile procedure should wait for a ClickHouse host: + # - to be excluded from a ClickHouse cluster + # - to complete all running queries + # - to be included into a ClickHouse cluster + # respectfully before moving forward with host reconcile + exclude: true + queries: true + include: false + # The operator during reconcile procedure should wait for replicas to catch-up + # replication delay a.k.a replication lag for the following replicas + replicas: + # All replicas (new and known earlier) are explicitly requested to wait for replication to catch-up + all: no + # New replicas only are requested to wait for replication to catch-up + new: yes + # Replication catch-up is considered to be completed as soon as replication delay + # a.k.a replication lag - calculated as "MAX(absolute_delay) FROM system.replicas" + # is within this specified delay (in seconds) + delay: 10 + probes: + # Whether the operator during host launch procedure should wait for startup probe to succeed. + # In case probe is unspecified wait is assumed to be completed successfully. + # Default option value is to do not wait. + startup: no + # Whether the operator during host launch procedure should wait for readiness probe to succeed. + # In case probe is unspecified wait is assumed to be completed successfully. + # Default option value is to wait. + readiness: yes + # The operator during reconcile procedure should drop the following entities: + drop: + replicas: + # Whether the operator during reconcile procedure should drop replicas when replica is deleted + onDelete: yes + # Whether the operator during reconcile procedure should drop replicas when replica volume is lost + onLostVolume: yes + # Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + active: no + + ################################################ + ## + ## Annotations management section + ## + ################################################ + annotation: + # Applied when: + # 1. Propagating annotations from the CHI's `metadata.annotations` to child objects' `metadata.annotations`, + # 2. Propagating annotations from the CHI Template's `metadata.annotations` to CHI's `metadata.annotations`, + # Include annotations from the following list: + # Applied only when not empty. Empty list means "include all, no selection" + include: [] + # Exclude annotations from the following list: + exclude: [] + + ################################################ + ## + ## Labels management section + ## + ################################################ + label: + # Applied when: + # 1. Propagating labels from the CHI's `metadata.labels` to child objects' `metadata.labels`, + # 2. Propagating labels from the CHI Template's `metadata.labels` to CHI's `metadata.labels`, + # Include labels from the following list: + # Applied only when not empty. Empty list means "include all, no selection" + include: [] + # Exclude labels from the following list: + # Applied only when not empty. Empty list means "nothing to exclude, no selection" + exclude: [] + # Whether to append *Scope* labels to StatefulSet and Pod. + # Full list of available *scope* labels check in 'labeler.go' + # LabelShardScopeIndex + # LabelReplicaScopeIndex + # LabelCHIScopeIndex + # LabelCHIScopeCycleSize + # LabelCHIScopeCycleIndex + # LabelCHIScopeCycleOffset + # LabelClusterScopeIndex + # LabelClusterScopeCycleSize + # LabelClusterScopeCycleIndex + # LabelClusterScopeCycleOffset + appendScope: "no" + + ################################################ + ## + ## Metrics management section + ## + ################################################ + metrics: + labels: + exclude: [] + + ################################################ + ## + ## Status management section + ## + ################################################ + status: + fields: + action: false + actions: false + error: true + errors: true + + ################################################ + ## + ## StatefulSet management section + ## + ################################################ + statefulSet: + revisionHistoryLimit: 0 + + ################################################ + ## + ## Pod management section + ## + ################################################ + pod: + # Grace period for Pod termination. + # How many seconds to wait between sending + # SIGTERM and SIGKILL during Pod termination process. + # Increase this number is case of slow shutdown. + terminationGracePeriod: 30 + + ################################################ + ## + ## Log parameters section + ## + ################################################ + logger: + logtostderr: "true" + alsologtostderr: "false" + v: "1" + stderrthreshold: "" + vmodule: "" + log_backtrace_at: "" + +--- +# Template Parameters: +# +# NAME=etc-clickhouse-operator-confd-files +# NAMESPACE=kube-system +# COMMENT= +# +apiVersion: v1 +kind: ConfigMap +metadata: + name: etc-clickhouse-operator-confd-files + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 + app: clickhouse-operator +data: +--- +# Template Parameters: +# +# NAME=etc-clickhouse-operator-configd-files +# NAMESPACE=kube-system +# COMMENT= +# +apiVersion: v1 +kind: ConfigMap +metadata: + name: etc-clickhouse-operator-configd-files + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 + app: clickhouse-operator +data: + 01-clickhouse-01-listen.xml: | + <!-- IMPORTANT --> + <!-- This file is auto-generated --> + <!-- Do not edit this file - all changes would be lost --> + <!-- Edit appropriate template in the following folder: --> + <!-- deploy/builder/templates-config --> + <!-- IMPORTANT --> + <yandex> + <!-- Listen wildcard address to allow accepting connections from other containers and host network. --> + <listen_host>::</listen_host> + <listen_host>0.0.0.0</listen_host> + <listen_try>1</listen_try> + </yandex> + + 01-clickhouse-02-logger.xml: | + <!-- IMPORTANT --> + <!-- This file is auto-generated --> + <!-- Do not edit this file - all changes would be lost --> + <!-- Edit appropriate template in the following folder: --> + <!-- deploy/builder/templates-config --> + <!-- IMPORTANT --> + <yandex> + <logger> + <!-- Possible levels: https://github.com/pocoproject/poco/blob/devel/Foundation/include/Poco/Logger.h#L439 --> + <level>debug</level> + <log>/var/log/clickhouse-server/clickhouse-server.log</log> + <errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog> + <size>1000M</size> + <count>10</count> + <!-- Default behavior is autodetection (log to console if not daemon mode and is tty) --> + <console>1</console> + </logger> + </yandex> + + 01-clickhouse-03-query_log.xml: | + <!-- IMPORTANT --> + <!-- This file is auto-generated --> + <!-- Do not edit this file - all changes would be lost --> + <!-- Edit appropriate template in the following folder: --> + <!-- deploy/builder/templates-config --> + <!-- IMPORTANT --> + <yandex> + <query_log replace="1"> + <database>system</database> + <table>query_log</table> + <engine>Engine = MergeTree PARTITION BY event_date ORDER BY event_time TTL event_date + interval 30 day</engine> + <flush_interval_milliseconds>7500</flush_interval_milliseconds> + </query_log> + <query_thread_log remove="1"/> + </yandex> + + 01-clickhouse-04-part_log.xml: | + <!-- IMPORTANT --> + <!-- This file is auto-generated --> + <!-- Do not edit this file - all changes would be lost --> + <!-- Edit appropriate template in the following folder: --> + <!-- deploy/builder/templates-config --> + <!-- IMPORTANT --> + <yandex> + <part_log replace="1"> + <database>system</database> + <table>part_log</table> + <engine>Engine = MergeTree PARTITION BY event_date ORDER BY event_time TTL event_date + interval 30 day</engine> + <flush_interval_milliseconds>7500</flush_interval_milliseconds> + </part_log> + </yandex> + + 01-clickhouse-05-trace_log.xml: | + <!-- IMPORTANT --> + <!-- This file is auto-generated --> + <!-- Do not edit this file - all changes would be lost --> + <!-- Edit appropriate template in the following folder: --> + <!-- deploy/builder/templates-config --> + <!-- IMPORTANT --> + <yandex> + <trace_log replace="1"> + <database>system</database> + <table>trace_log</table> + <engine>Engine = MergeTree PARTITION BY event_date ORDER BY event_time TTL event_date + interval 30 day</engine> + <flush_interval_milliseconds>7500</flush_interval_milliseconds> + </trace_log> + </yandex> + +--- +# Template Parameters: +# +# NAME=etc-clickhouse-operator-templatesd-files +# NAMESPACE=kube-system +# COMMENT= +# +apiVersion: v1 +kind: ConfigMap +metadata: + name: etc-clickhouse-operator-templatesd-files + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 + app: clickhouse-operator +data: + 001-templates.json.example: | + { + "apiVersion": "clickhouse.altinity.com/v1", + "kind": "ClickHouseInstallationTemplate", + "metadata": { + "name": "01-default-volumeclaimtemplate" + }, + "spec": { + "templates": { + "volumeClaimTemplates": [ + { + "name": "chi-default-volume-claim-template", + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "2Gi" + } + } + } + } + ], + "podTemplates": [ + { + "name": "chi-default-oneperhost-pod-template", + "distribution": "OnePerHost", + "spec": { + "containers" : [ + { + "name": "clickhouse", + "image": "clickhouse/clickhouse-server:23.8", + "ports": [ + { + "name": "http", + "containerPort": 8123 + }, + { + "name": "client", + "containerPort": 9000 + }, + { + "name": "interserver", + "containerPort": 9009 + } + ] + } + ] + } + } + ] + } + } + } + + default-pod-template.yaml.example: | + apiVersion: "clickhouse.altinity.com/v1" + kind: "ClickHouseInstallationTemplate" + metadata: + name: "default-oneperhost-pod-template" + spec: + templates: + podTemplates: + - name: default-oneperhost-pod-template + distribution: "OnePerHost" + default-storage-template.yaml.example: | + apiVersion: "clickhouse.altinity.com/v1" + kind: "ClickHouseInstallationTemplate" + metadata: + name: "default-storage-template-2Gi" + spec: + templates: + volumeClaimTemplates: + - name: default-storage-template-2Gi + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi + + readme: | + Templates in this folder are packaged with an operator and available via 'useTemplate' +--- +# Template Parameters: +# +# NAME=etc-clickhouse-operator-usersd-files +# NAMESPACE=kube-system +# COMMENT= +# +apiVersion: v1 +kind: ConfigMap +metadata: + name: etc-clickhouse-operator-usersd-files + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 + app: clickhouse-operator +data: + 01-clickhouse-operator-profile.xml: | + <!-- IMPORTANT --> + <!-- This file is auto-generated --> + <!-- Do not edit this file - all changes would be lost --> + <!-- Edit appropriate template in the following folder: --> + <!-- deploy/builder/templates-config --> + <!-- IMPORTANT --> + <!-- + # + # Template parameters available: + # + --> + <yandex> + <!-- clickhouse-operator user is generated by the operator based on config.yaml in runtime --> + <profiles> + <clickhouse_operator> + <log_queries>0</log_queries> + <skip_unavailable_shards>1</skip_unavailable_shards> + <http_connection_timeout>10</http_connection_timeout> + <max_concurrent_queries_for_all_users>0</max_concurrent_queries_for_all_users> + <os_thread_priority>0</os_thread_priority> + </clickhouse_operator> + </profiles> + </yandex> + + 02-clickhouse-default-profile.xml: | + <!-- IMPORTANT --> + <!-- This file is auto-generated --> + <!-- Do not edit this file - all changes would be lost --> + <!-- Edit appropriate template in the following folder: --> + <!-- deploy/builder/templates-config --> + <!-- IMPORTANT --> + <yandex> + <profiles> + <default> + <os_thread_priority>2</os_thread_priority> + <log_queries>1</log_queries> + <connect_timeout_with_failover_ms>1000</connect_timeout_with_failover_ms> + <distributed_aggregation_memory_efficient>1</distributed_aggregation_memory_efficient> + <parallel_view_processing>1</parallel_view_processing> + <do_not_merge_across_partitions_select_final>1</do_not_merge_across_partitions_select_final> + <load_balancing>nearest_hostname</load_balancing> + <prefer_localhost_replica>0</prefer_localhost_replica> + <!-- materialize_ttl_recalculate_only>1</materialize_ttl_recalculate_only> 21.10 and above --> + </default> + </profiles> + </yandex> +--- +# Template Parameters: +# +# NAME=etc-keeper-operator-confd-files +# NAMESPACE=kube-system +# COMMENT= +# +apiVersion: v1 +kind: ConfigMap +metadata: + name: etc-keeper-operator-confd-files + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 + app: clickhouse-operator +data: +--- +# Template Parameters: +# +# NAME=etc-keeper-operator-configd-files +# NAMESPACE=kube-system +# COMMENT= +# +apiVersion: v1 +kind: ConfigMap +metadata: + name: etc-keeper-operator-configd-files + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 + app: clickhouse-operator +data: + 01-keeper-01-default-config.xml: | + <!-- IMPORTANT --> + <!-- This file is auto-generated --> + <!-- Do not edit this file - all changes would be lost --> + <!-- Edit appropriate template in the following folder: --> + <!-- deploy/builder/templates-config --> + <!-- IMPORTANT --> + <clickhouse> + <asynchronous_metrics_keeper_metrics_only>1</asynchronous_metrics_keeper_metrics_only> + <keeper_server> + <coordination_settings> + <min_session_timeout_ms>10000</min_session_timeout_ms> + <operation_timeout_ms>10000</operation_timeout_ms> + <raft_logs_level>information</raft_logs_level> + <session_timeout_ms>100000</session_timeout_ms> + </coordination_settings> + <hostname_checks_enabled>true</hostname_checks_enabled> + <log_storage_path>/var/lib/clickhouse-keeper/coordination/logs</log_storage_path> + <snapshot_storage_path>/var/lib/clickhouse-keeper/coordination/snapshots</snapshot_storage_path> + <storage_path>/var/lib/clickhouse-keeper</storage_path> + <tcp_port>2181</tcp_port> + </keeper_server> + <listen_host>::</listen_host> + <listen_host>0.0.0.0</listen_host> + <listen_try>1</listen_try> + <logger> + <console>1</console> + <level>information</level> + </logger> + <max_connections>4096</max_connections> + </clickhouse> + + 01-keeper-02-readiness.xml: | + <!-- IMPORTANT --> + <!-- This file is auto-generated --> + <!-- Do not edit this file - all changes would be lost --> + <!-- Edit appropriate template in the following folder: --> + <!-- deploy/builder/templates-config --> + <!-- IMPORTANT --> + <clickhouse> + <keeper_server> + <http_control> + <port>9182</port> + <readiness> + <endpoint>/ready</endpoint> + </readiness> + </http_control> + </keeper_server> + </clickhouse> + + 01-keeper-03-enable-reconfig.xml: | + <!-- IMPORTANT --> + <!-- This file is auto-generated --> + <!-- Do not edit this file - all changes would be lost --> + <!-- Edit appropriate template in the following folder: --> + <!-- deploy/builder/templates-config --> + <!-- IMPORTANT --> + <clickhouse> + <keeper_server> + <enable_reconfiguration>false</enable_reconfiguration> + </keeper_server> + </clickhouse> + +--- +# Template Parameters: +# +# NAME=etc-keeper-operator-templatesd-files +# NAMESPACE=kube-system +# COMMENT= +# +apiVersion: v1 +kind: ConfigMap +metadata: + name: etc-keeper-operator-templatesd-files + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 + app: clickhouse-operator +data: + readme: | + Templates in this folder are packaged with an operator and available via 'useTemplate' +--- +# Template Parameters: +# +# NAME=etc-keeper-operator-usersd-files +# NAMESPACE=kube-system +# COMMENT= +# +apiVersion: v1 +kind: ConfigMap +metadata: + name: etc-keeper-operator-usersd-files + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 + app: clickhouse-operator +data: +--- +# +# Template parameters available: +# NAMESPACE=kube-system +# COMMENT= +# OPERATOR_VERSION=0.26.3 +# CH_USERNAME_SECRET_PLAIN=clickhouse_operator +# CH_PASSWORD_SECRET_PLAIN=clickhouse_operator_password +# +apiVersion: v1 +kind: Secret +metadata: + name: clickhouse-operator + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 + app: clickhouse-operator +type: Opaque +stringData: + username: clickhouse_operator + password: clickhouse_operator_password +--- +# Template Parameters: +# +# NAMESPACE=kube-system +# COMMENT= +# OPERATOR_IMAGE=altinity/clickhouse-operator:0.26.3 +# OPERATOR_IMAGE_PULL_POLICY=Always +# METRICS_EXPORTER_IMAGE=altinity/metrics-exporter:0.26.3 +# METRICS_EXPORTER_IMAGE_PULL_POLICY=Always +# +# Setup Deployment for clickhouse-operator +# Deployment would be created in kubectl-specified namespace +kind: Deployment +apiVersion: apps/v1 +metadata: + name: clickhouse-operator + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 + app: clickhouse-operator +spec: + replicas: 1 + selector: + matchLabels: + app: clickhouse-operator + template: + metadata: + labels: + app: clickhouse-operator + annotations: + prometheus.io/port: '8888' + prometheus.io/scrape: 'true' + clickhouse-operator-metrics/port: '9999' + clickhouse-operator-metrics/scrape: 'true' + spec: + serviceAccountName: clickhouse-operator + volumes: + - name: etc-clickhouse-operator-folder + configMap: + name: etc-clickhouse-operator-files + - name: etc-clickhouse-operator-confd-folder + configMap: + name: etc-clickhouse-operator-confd-files + - name: etc-clickhouse-operator-configd-folder + configMap: + name: etc-clickhouse-operator-configd-files + - name: etc-clickhouse-operator-templatesd-folder + configMap: + name: etc-clickhouse-operator-templatesd-files + - name: etc-clickhouse-operator-usersd-folder + configMap: + name: etc-clickhouse-operator-usersd-files + - name: etc-keeper-operator-confd-folder + configMap: + name: etc-keeper-operator-confd-files + - name: etc-keeper-operator-configd-folder + configMap: + name: etc-keeper-operator-configd-files + - name: etc-keeper-operator-templatesd-folder + configMap: + name: etc-keeper-operator-templatesd-files + - name: etc-keeper-operator-usersd-folder + configMap: + name: etc-keeper-operator-usersd-files + containers: + - name: clickhouse-operator + image: altinity/clickhouse-operator:0.26.3 + imagePullPolicy: Always + volumeMounts: + - name: etc-clickhouse-operator-folder + mountPath: /etc/clickhouse-operator + - name: etc-clickhouse-operator-confd-folder + mountPath: /etc/clickhouse-operator/chi/conf.d + - name: etc-clickhouse-operator-configd-folder + mountPath: /etc/clickhouse-operator/chi/config.d + - name: etc-clickhouse-operator-templatesd-folder + mountPath: /etc/clickhouse-operator/chi/templates.d + - name: etc-clickhouse-operator-usersd-folder + mountPath: /etc/clickhouse-operator/chi/users.d + - name: etc-keeper-operator-confd-folder + mountPath: /etc/clickhouse-operator/chk/conf.d + - name: etc-keeper-operator-configd-folder + mountPath: /etc/clickhouse-operator/chk/keeper_config.d + - name: etc-keeper-operator-templatesd-folder + mountPath: /etc/clickhouse-operator/chk/templates.d + - name: etc-keeper-operator-usersd-folder + mountPath: /etc/clickhouse-operator/chk/users.d + env: + # Pod-specific + # spec.nodeName: ip-172-20-52-62.ec2.internal + - name: OPERATOR_POD_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + # metadata.name: clickhouse-operator-6f87589dbb-ftcsf + - name: OPERATOR_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + # metadata.namespace: kube-system + - name: OPERATOR_POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + # status.podIP: 100.96.3.2 + - name: OPERATOR_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + # spec.serviceAccount: clickhouse-operator + # spec.serviceAccountName: clickhouse-operator + - name: OPERATOR_POD_SERVICE_ACCOUNT + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName + + # Container-specific + - name: OPERATOR_CONTAINER_CPU_REQUEST + valueFrom: + resourceFieldRef: + containerName: clickhouse-operator + resource: requests.cpu + - name: OPERATOR_CONTAINER_CPU_LIMIT + valueFrom: + resourceFieldRef: + containerName: clickhouse-operator + resource: limits.cpu + - name: OPERATOR_CONTAINER_MEM_REQUEST + valueFrom: + resourceFieldRef: + containerName: clickhouse-operator + resource: requests.memory + - name: OPERATOR_CONTAINER_MEM_LIMIT + valueFrom: + resourceFieldRef: + containerName: clickhouse-operator + resource: limits.memory + ports: + - containerPort: 9999 + name: op-metrics + + - name: metrics-exporter + image: altinity/metrics-exporter:0.26.3 + imagePullPolicy: Always + volumeMounts: + - name: etc-clickhouse-operator-folder + mountPath: /etc/clickhouse-operator + - name: etc-clickhouse-operator-confd-folder + mountPath: /etc/clickhouse-operator/chi/conf.d + - name: etc-clickhouse-operator-configd-folder + mountPath: /etc/clickhouse-operator/chi/config.d + - name: etc-clickhouse-operator-templatesd-folder + mountPath: /etc/clickhouse-operator/chi/templates.d + - name: etc-clickhouse-operator-usersd-folder + mountPath: /etc/clickhouse-operator/chi/users.d + - name: etc-keeper-operator-confd-folder + mountPath: /etc/clickhouse-operator/chk/conf.d + - name: etc-keeper-operator-configd-folder + mountPath: /etc/clickhouse-operator/chk/keeper_config.d + - name: etc-keeper-operator-templatesd-folder + mountPath: /etc/clickhouse-operator/chk/templates.d + - name: etc-keeper-operator-usersd-folder + mountPath: /etc/clickhouse-operator/chk/users.d + env: + # Pod-specific + # spec.nodeName: ip-172-20-52-62.ec2.internal + - name: OPERATOR_POD_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + # metadata.name: clickhouse-operator-6f87589dbb-ftcsf + - name: OPERATOR_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + # metadata.namespace: kube-system + - name: OPERATOR_POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + # status.podIP: 100.96.3.2 + - name: OPERATOR_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + # spec.serviceAccount: clickhouse-operator + # spec.serviceAccountName: clickhouse-operator + - name: OPERATOR_POD_SERVICE_ACCOUNT + valueFrom: + fieldRef: + fieldPath: spec.serviceAccountName + + # Container-specific + - name: OPERATOR_CONTAINER_CPU_REQUEST + valueFrom: + resourceFieldRef: + containerName: clickhouse-operator + resource: requests.cpu + - name: OPERATOR_CONTAINER_CPU_LIMIT + valueFrom: + resourceFieldRef: + containerName: clickhouse-operator + resource: limits.cpu + - name: OPERATOR_CONTAINER_MEM_REQUEST + valueFrom: + resourceFieldRef: + containerName: clickhouse-operator + resource: requests.memory + - name: OPERATOR_CONTAINER_MEM_LIMIT + valueFrom: + resourceFieldRef: + containerName: clickhouse-operator + resource: limits.memory + ports: + - containerPort: 8888 + name: ch-metrics +--- +# Template Parameters: +# +# NAMESPACE=kube-system +# COMMENT= +# +# Setup ClusterIP Service to provide monitoring metrics for Prometheus +# Service would be created in kubectl-specified namespace +# In order to get access outside of k8s it should be exposed as: +# kubectl --namespace prometheus port-forward service/prometheus 9090 +# and point browser to localhost:9090 +kind: Service +apiVersion: v1 +metadata: + name: clickhouse-operator-metrics + namespace: kube-system + labels: + clickhouse.altinity.com/chop: 0.26.3 + app: clickhouse-operator +spec: + ports: + - port: 8888 + name: ch-metrics + - port: 9999 + name: op-metrics + selector: + app: clickhouse-operator diff --git a/pkg/vendored/altinity-clickhouse/crds/clickhouse.altinity.com_clickhouseinstallations.yaml b/pkg/vendored/altinity-clickhouse/crds/clickhouse.altinity.com_clickhouseinstallations.yaml new file mode 100644 index 00000000..8bba1545 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/crds/clickhouse.altinity.com_clickhouseinstallations.yaml @@ -0,0 +1,9468 @@ +# Template Parameters: +# +# KIND=ClickHouseInstallation +# SINGULAR=clickhouseinstallation +# PLURAL=clickhouseinstallations +# SHORT=chi +# OPERATOR_VERSION=0.26.3 +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clickhouseinstallations.clickhouse.altinity.com + labels: + clickhouse.altinity.com/chop: 0.26.3 +spec: + group: clickhouse.altinity.com + scope: Namespaced + names: + kind: ClickHouseInstallation + singular: clickhouseinstallation + plural: clickhouseinstallations + shortNames: + - chi + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: status + type: string + description: Resource status + jsonPath: .status.status + - name: version + type: string + description: Operator version + priority: 1 # show in wide view + jsonPath: .status.chop-version + - name: clusters + type: integer + description: Clusters count + jsonPath: .status.clusters + - name: shards + type: integer + description: Shards count + priority: 1 # show in wide view + jsonPath: .status.shards + - name: hosts + type: integer + description: Hosts count + jsonPath: .status.hosts + - name: taskID + type: string + description: TaskID + priority: 1 # show in wide view + jsonPath: .status.taskID + - name: hosts-completed + type: integer + description: Completed hosts count + jsonPath: .status.hostsCompleted + - name: hosts-updated + type: integer + description: Updated hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsUpdated + - name: hosts-added + type: integer + description: Added hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsAdded + - name: hosts-deleted + type: integer + description: Hosts deleted count + priority: 1 # show in wide view + jsonPath: .status.hostsDeleted + - name: endpoint + type: string + description: Client access endpoint + priority: 1 # show in wide view + jsonPath: .status.endpoint + - name: age + type: date + description: Age of the resource + # Displayed in all priorities + jsonPath: .metadata.creationTimestamp + - name: suspend + type: string + description: Suspend reconciliation + # Displayed in all priorities + jsonPath: .spec.suspend + subresources: + status: {} + schema: + openAPIV3Schema: + description: "define a set of Kubernetes resources (StatefulSet, PVC, Service, ConfigMap) which describe behavior one or more clusters" + type: object + required: + - spec + properties: + apiVersion: + description: | + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: | + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + type: object + description: | + Status contains many fields like a normalized configuration, clickhouse-operator version, current action and all applied action list, current taskID and all applied taskIDs and other + properties: + chop-version: + type: string + description: "Operator version" + chop-commit: + type: string + description: "Operator git commit SHA" + chop-date: + type: string + description: "Operator build date" + chop-ip: + type: string + description: "IP address of the operator's pod which managed this resource" + clusters: + type: integer + minimum: 0 + description: "Clusters count" + shards: + type: integer + minimum: 0 + description: "Shards count" + replicas: + type: integer + minimum: 0 + description: "Replicas count" + hosts: + type: integer + minimum: 0 + description: "Hosts count" + status: + type: string + description: "Status" + taskID: + type: string + description: "Current task id" + taskIDsStarted: + type: array + description: "Started task ids" + nullable: true + items: + type: string + taskIDsCompleted: + type: array + description: "Completed task ids" + nullable: true + items: + type: string + action: + type: string + description: "Action" + actions: + type: array + description: "Actions" + nullable: true + items: + type: string + error: + type: string + description: "Last error" + errors: + type: array + description: "Errors" + nullable: true + items: + type: string + hostsUnchanged: + type: integer + minimum: 0 + description: "Unchanged Hosts count" + hostsUpdated: + type: integer + minimum: 0 + description: "Updated Hosts count" + hostsAdded: + type: integer + minimum: 0 + description: "Added Hosts count" + hostsCompleted: + type: integer + minimum: 0 + description: "Completed Hosts count" + hostsDeleted: + type: integer + minimum: 0 + description: "Deleted Hosts count" + hostsDelete: + type: integer + minimum: 0 + description: "About to delete Hosts count" + pods: + type: array + description: "Pods" + nullable: true + items: + type: string + pod-ips: + type: array + description: "Pod IPs" + nullable: true + items: + type: string + fqdns: + type: array + description: "Pods FQDNs" + nullable: true + items: + type: string + endpoint: + type: string + description: "Endpoint" + endpoints: + type: array + description: "All endpoints" + nullable: true + items: + type: string + generation: + type: integer + minimum: 0 + description: "Generation" + normalized: + type: object + description: "Normalized resource requested" + nullable: true + x-kubernetes-preserve-unknown-fields: true + normalizedCompleted: + type: object + description: "Normalized resource completed" + nullable: true + x-kubernetes-preserve-unknown-fields: true + actionPlan: + type: object + description: "Action Plan" + nullable: true + x-kubernetes-preserve-unknown-fields: true + hostsWithTablesCreated: + type: array + description: "List of hosts with tables created by the operator" + nullable: true + items: + type: string + hostsWithReplicaCaughtUp: + type: array + description: "List of hosts with replica caught up" + nullable: true + items: + type: string + usedTemplates: + type: array + description: "List of templates used to build this CHI" + nullable: true + x-kubernetes-preserve-unknown-fields: true + items: + type: object + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + # x-kubernetes-preserve-unknown-fields: true + description: | + Specification of the desired behavior of one or more ClickHouse clusters + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md + properties: + taskID: + type: string + description: | + Allows to define custom taskID for CHI update and watch status of this update execution. + Displayed in all .status.taskID* fields. + By default (if not filled) every update of CHI manifest will generate random taskID + stop: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + restart: + type: string + description: | + In case 'RollingUpdate' specified, the operator will always restart ClickHouse pods during reconcile. + This options is used in rare cases when force restart is required and is typically removed after the use in order to avoid unneeded restarts. + enum: + - "" + - "RollingUpdate" + suspend: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Suspend reconciliation of resources managed by a ClickHouse Installation. + Works as the following: + - When `suspend` is `true` operator stops reconciling all resources. + - When `suspend` is `false` or not set, operator reconciles all resources. + troubleshoot: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Allows to troubleshoot Pods during CrashLoopBack state. + This may happen when wrong configuration applied, in this case `clickhouse-server` wouldn't start. + Command within ClickHouse container is modified with `sleep` in order to avoid quick restarts + and give time to troubleshoot via CLI. + Liveness and Readiness probes are disabled as well. + namespaceDomainPattern: + type: string + description: | + Custom domain pattern which will be used for DNS names of `Service` or `Pod`. + Typical use scenario - custom cluster domain in Kubernetes cluster + Example: %s.svc.my.test + templating: + type: object + # nullable: true + description: | + Optional, applicable inside ClickHouseInstallationTemplate only. + Defines current ClickHouseInstallationTemplate application options to target ClickHouseInstallation(s)." + properties: + policy: + type: string + description: | + When defined as `auto` inside ClickhouseInstallationTemplate, this ClickhouseInstallationTemplate + will be auto-added into ClickHouseInstallation, selectable by `chiSelector`. + Default value is `manual`, meaning ClickHouseInstallation should request this ClickhouseInstallationTemplate explicitly. + enum: + - "" + - "auto" + - "manual" + chiSelector: + type: object + description: "Optional, defines selector for ClickHouseInstallation(s) to be templated with ClickhouseInstallationTemplate" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + reconciling: + type: object + description: "[OBSOLETED] Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + # nullable: true + properties: + policy: + type: string + description: | + DISCUSSED TO BE DEPRECATED + Syntax sugar + Overrides all three 'reconcile.host.wait.{exclude, queries, include}' values from the operator's config + Possible values: + - wait - should wait to exclude host, complete queries and include host back into the cluster + - nowait - should NOT wait to exclude host, complete queries and include host back into the cluster + enum: + - "" + - "wait" + - "nowait" + configMapPropagationTimeout: + type: integer + description: | + Timeout in seconds for `clickhouse-operator` to wait for modified `ConfigMap` to propagate into the `Pod` + More details: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically + minimum: 0 + maximum: 3600 + cleanup: + type: object + description: "Optional, defines behavior for cleanup Kubernetes resources during reconcile cycle" + # nullable: true + properties: + unknownObjects: + type: object + description: | + Describes what clickhouse-operator should do with found Kubernetes resources which should be managed by clickhouse-operator, + but do not have `ownerReference` to any currently managed `ClickHouseInstallation` resource. + Default behavior is `Delete`" + # nullable: true + properties: + statefulSet: + type: string + description: "Behavior policy for unknown StatefulSet, `Delete` by default" + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown PVC, `Delete` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown ConfigMap, `Delete` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown Service, `Delete` by default" + reconcileFailedObjects: + type: object + description: | + Describes what clickhouse-operator should do with Kubernetes resources which are failed during reconcile. + Default behavior is `Retain`" + # nullable: true + properties: + statefulSet: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed StatefulSet, `Retain` by default" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed PVC, `Retain` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed ConfigMap, `Retain` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed Service, `Retain` by default" + macros: + type: object + description: "macros parameters" + properties: + sections: + type: object + description: "sections behaviour for macros" + properties: + users: + type: object + description: "sections behaviour for macros on users" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + profiles: + type: object + description: "sections behaviour for macros on profiles" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + quotas: + type: object + description: "sections behaviour for macros on quotas" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + settings: + type: object + description: "sections behaviour for macros on settings" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + files: + type: object + description: "sections behaviour for macros on files" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + statefulSet: + type: object + description: "Optional, StatefulSet reconcile behavior tuning" + properties: + create: + type: object + description: "Behavior during create StatefulSet" + properties: + onFailure: + type: string + description: | + What to do in case created StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. delete - delete newly created problematic StatefulSet and follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "delete" + - "ignore" + update: + type: object + description: "Behavior during update StatefulSet" + properties: + timeout: + type: integer + description: "How many seconds to wait for StatefulSet to be 'Ready' during update" + minimum: 0 + maximum: 3600 + pollInterval: + type: integer + description: "How many seconds to wait between checks for StatefulSet status during update" + minimum: 1 + maximum: 600 + onFailure: + type: string + description: | + What to do in case updated StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. rollback - delete Pod and rollback StatefulSet to previous Generation. Follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "rollback" + - "ignore" + recreate: + type: object + description: "Behavior during recreate StatefulSet" + properties: + onDataLoss: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to PVC data loss or missing volumes. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + onUpdateFailure: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to update failure or StatefulSet not ready. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + reconcile: + type: object + # nullable: true + properties: + policy: + type: string + description: | + DISCUSSED TO BE DEPRECATED + Syntax sugar + Overrides all three 'reconcile.host.wait.{exclude, queries, include}' values from the operator's config + Possible values: + - wait - should wait to exclude host, complete queries and include host back into the cluster + - nowait - should NOT wait to exclude host, complete queries and include host back into the cluster + enum: + - "" + - "wait" + - "nowait" + configMapPropagationTimeout: + type: integer + description: | + Timeout in seconds for `clickhouse-operator` to wait for modified `ConfigMap` to propagate into the `Pod` + More details: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically + minimum: 0 + maximum: 3600 + cleanup: + type: object + description: "Optional, defines behavior for cleanup Kubernetes resources during reconcile cycle" + # nullable: true + properties: + unknownObjects: + type: object + description: | + Describes what clickhouse-operator should do with found Kubernetes resources which should be managed by clickhouse-operator, + but do not have `ownerReference` to any currently managed `ClickHouseInstallation` resource. + Default behavior is `Delete`" + # nullable: true + properties: + statefulSet: + type: string + description: "Behavior policy for unknown StatefulSet, `Delete` by default" + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown PVC, `Delete` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown ConfigMap, `Delete` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown Service, `Delete` by default" + reconcileFailedObjects: + type: object + description: | + Describes what clickhouse-operator should do with Kubernetes resources which are failed during reconcile. + Default behavior is `Retain`" + # nullable: true + properties: + statefulSet: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed StatefulSet, `Retain` by default" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed PVC, `Retain` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed ConfigMap, `Retain` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed Service, `Retain` by default" + macros: + type: object + description: "macros parameters" + properties: + sections: + type: object + description: "sections behaviour for macros" + properties: + users: + type: object + description: "sections behaviour for macros on users" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + profiles: + type: object + description: "sections behaviour for macros on profiles" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + quotas: + type: object + description: "sections behaviour for macros on quotas" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + settings: + type: object + description: "sections behaviour for macros on settings" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + files: + type: object + description: "sections behaviour for macros on files" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + statefulSet: + type: object + description: "Optional, StatefulSet reconcile behavior tuning" + properties: + create: + type: object + description: "Behavior during create StatefulSet" + properties: + onFailure: + type: string + description: | + What to do in case created StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. delete - delete newly created problematic StatefulSet and follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "delete" + - "ignore" + update: + type: object + description: "Behavior during update StatefulSet" + properties: + timeout: + type: integer + description: "How many seconds to wait for StatefulSet to be 'Ready' during update" + minimum: 0 + maximum: 3600 + pollInterval: + type: integer + description: "How many seconds to wait between checks for StatefulSet status during update" + minimum: 1 + maximum: 600 + onFailure: + type: string + description: | + What to do in case updated StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. rollback - delete Pod and rollback StatefulSet to previous Generation. Follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "rollback" + - "ignore" + recreate: + type: object + description: "Behavior during recreate StatefulSet" + properties: + onDataLoss: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to PVC data loss or missing volumes. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + onUpdateFailure: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to update failure or StatefulSet not ready. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + description: "Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + defaults: + type: object + description: | + define default behavior for whole ClickHouseInstallation, some behavior can be re-define on cluster, shard and replica level + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specdefaults + # nullable: true + properties: + replicasUseFQDN: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + define should replicas be specified by FQDN in `<host></host>`. + In case of "no" will use short hostname and clickhouse-server will use kubernetes default suffixes for DNS lookup + "no" by default + distributedDDL: + type: object + description: | + allows change `<yandex><distributed_ddl></distributed_ddl></yandex>` settings + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings-distributed_ddl + # nullable: true + properties: + profile: + type: string + description: "Settings from this profile will be used to execute DDL queries" + storageManagement: + type: object + description: default storage management options + properties: + provisioner: + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + templates: + type: object + description: "optional, configuration of the templates names which will use for generate Kubernetes resources according to one or more ClickHouse clusters described in current ClickHouseInstallation (chi) resource" + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + configuration: + type: object + description: "allows configure multiple aspects and behavior for `clickhouse-server` instance and also allows describe multiple `clickhouse-server` clusters inside one `chi` resource" + # nullable: true + properties: + zookeeper: + type: object + description: | + allows configure <yandex><zookeeper>..</zookeeper></yandex> section in each `Pod` during generate `ConfigMap` which will mounted in `/etc/clickhouse-server/config.d/` + `clickhouse-operator` itself doesn't manage Zookeeper, please install Zookeeper separatelly look examples on https://github.com/Altinity/clickhouse-operator/tree/master/deploy/zookeeper/ + currently, zookeeper (or clickhouse-keeper replacement) used for *ReplicatedMergeTree table engines and for `distributed_ddl` + More details: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings_zookeeper + # nullable: true + properties: + nodes: + type: array + description: "describe every available zookeeper cluster node for interaction" + # nullable: true + items: + type: object + #required: + # - host + properties: + host: + type: string + description: "dns name or ip address for Zookeeper node" + port: + type: integer + description: "TCP port which used to connect to Zookeeper node" + minimum: 0 + maximum: 65535 + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "if a secure connection to Zookeeper is required" + availabilityZone: + type: string + description: "availability zone for Zookeeper node" + session_timeout_ms: + type: integer + description: "session timeout during connect to Zookeeper" + operation_timeout_ms: + type: integer + description: "one operation timeout during Zookeeper transactions" + root: + type: string + description: "optional root znode path inside zookeeper to store ClickHouse related data (replication queue or distributed DDL)" + identity: + type: string + description: "optional access credentials string with `user:password` format used when use digest authorization in Zookeeper" + use_compression: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Enables compression in Keeper protocol if set to true" + users: + type: object + description: | + allows configure <yandex><users>..</users></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure password hashed, authorization restrictions, database level security row filters etc. + More details: https://clickhouse.tech/docs/en/operations/settings/settings-users/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationusers + + any key could contains `valueFrom` with `secretKeyRef` which allow pass password from kubernetes secrets + secret value will pass in `pod.spec.containers.evn`, and generate with from_env=XXX in XML in /etc/clickhouse-server/users.d/chop-generated-users.xml + it not allow automatically updates when updates `secret`, change spec.taskID for manually trigger reconcile cycle + + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + any key with prefix `k8s_secret_` shall has value with format namespace/secret/key or secret/key + in this case value from secret will write directly into XML tag during render *-usersd ConfigMap + + any key with prefix `k8s_secret_env` shall has value with format namespace/secret/key or secret/key + in this case value from secret will write into environment variable and write to XML tag via from_env=XXX + + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + # nullable: true + x-kubernetes-preserve-unknown-fields: true + profiles: + type: object + description: | + allows configure <yandex><profiles>..</profiles></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure any aspect of settings profile + More details: https://clickhouse.tech/docs/en/operations/settings/settings-profiles/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationprofiles + # nullable: true + x-kubernetes-preserve-unknown-fields: true + quotas: + type: object + description: | + allows configure <yandex><quotas>..</quotas></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure any aspect of resource quotas + More details: https://clickhouse.tech/docs/en/operations/quotas/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationquotas + # nullable: true + x-kubernetes-preserve-unknown-fields: true + settings: + type: object + description: | + allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationsettings + + any key could contains `valueFrom` with `secretKeyRef` which allow pass password from kubernetes secrets + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + secret value will pass in `pod.spec.env`, and generate with from_env=XXX in XML in /etc/clickhouse-server/config.d/chop-generated-settings.xml + it not allow automatically updates when updates `secret`, change spec.taskID for manually trigger reconcile cycle + # nullable: true + x-kubernetes-preserve-unknown-fields: true + files: + type: object + description: | + allows define content of any setting file inside each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + every key in this object is the file name + every value in this object is the file content + you can use `!!binary |` and base64 for binary files, see details here https://yaml.org/type/binary.html + each key could contains prefix like {common}, {users}, {hosts} or config.d, users.d, conf.d, wrong prefixes will be ignored, subfolders also will be ignored + More details: https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-05-files-nested.yaml + + any key could contains `valueFrom` with `secretKeyRef` which allow pass values from kubernetes secrets + secrets will mounted into pod as separate volume in /etc/clickhouse-server/secrets.d/ + and will automatically update when update secret + it useful for pass SSL certificates from cert-manager or similar tool + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + # nullable: true + x-kubernetes-preserve-unknown-fields: true + clusters: + type: array + description: | + describes clusters layout and allows change settings on cluster-level, shard-level and replica-level + every cluster is a set of StatefulSet, one StatefulSet contains only one Pod with `clickhouse-server` + all Pods will rendered in <remote_server> part of ClickHouse configs, mounted from ConfigMap as `/etc/clickhouse-server/config.d/chop-generated-remote_servers.xml` + Clusters will use for Distributed table engine, more details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + If `cluster` contains zookeeper settings (could be inherited from top `chi` level), when you can create *ReplicatedMergeTree tables + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "cluster name, used to identify set of servers and wide used during generate names of related Kubernetes resources" + minLength: 1 + # See namePartClusterMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + zookeeper: + type: object + # nullable: true + properties: + nodes: + type: array + description: "describe every available zookeeper cluster node for interaction" + # nullable: true + items: + type: object + #required: + # - host + properties: + host: + type: string + description: "dns name or ip address for Zookeeper node" + port: + type: integer + description: "TCP port which used to connect to Zookeeper node" + minimum: 0 + maximum: 65535 + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "if a secure connection to Zookeeper is required" + availabilityZone: + type: string + description: "availability zone for Zookeeper node" + session_timeout_ms: + type: integer + description: "session timeout during connect to Zookeeper" + operation_timeout_ms: + type: integer + description: "one operation timeout during Zookeeper transactions" + root: + type: string + description: "optional root znode path inside zookeeper to store ClickHouse related data (replication queue or distributed DDL)" + identity: + type: string + description: "optional access credentials string with `user:password` format used when use digest authorization in Zookeeper" + use_compression: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Enables compression in Keeper protocol if set to true" + description: | + optional, allows configure <yandex><zookeeper>..</zookeeper></yandex> section in each `Pod` only in current ClickHouse cluster, during generate `ConfigMap` which will mounted in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.zookeeper` settings + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` on current cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected cluster + override top-level `chi.spec.configuration.templates` + schemaPolicy: + type: object + description: | + describes how schema is propagated within replicas and shards + properties: + replica: + type: string + description: "how schema is propagated within a replica" + enum: + # List SchemaPolicyReplicaXXX constants from model + - "" + - "None" + - "All" + shard: + type: string + description: "how schema is propagated between shards" + enum: + # List SchemaPolicyShardXXX constants from model + - "" + - "None" + - "All" + - "DistributedTablesOnly" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: optional, open secure ports for cluster + secret: + type: object + description: "optional, shared secret value to secure cluster communications" + properties: + auto: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Auto-generate shared secret value to secure cluster communications" + value: + description: "Cluster shared secret value in plain text" + type: string + valueFrom: + description: "Cluster shared secret source" + type: object + properties: + secretKeyRef: + description: | + Selects a key of a secret in the clickhouse installation namespace. + Should not be used if value is not empty. + type: object + properties: + name: + description: | + Name of the referent. More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + key: + description: The key of the secret to select from. Must be a valid secret key. + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - name + - key + pdbManaged: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Specifies whether the Pod Disruption Budget (PDB) should be managed. + During the next installation, if PDB management is enabled, the operator will + attempt to retrieve any existing PDB. If none is found, it will create a new one + and initiate a reconciliation loop. If PDB management is disabled, the existing PDB + will remain intact, and the reconciliation loop will not be executed. By default, + PDB management is enabled. + pdbMaxUnavailable: + type: integer + description: | + Pod eviction is allowed if at most "pdbMaxUnavailable" pods are unavailable after the eviction, + i.e. even in absence of the evicted pod. For example, one can prevent all voluntary evictions + by specifying 0. This is a mutually exclusive setting with "minAvailable". + minimum: 0 + maximum: 65535 + reconcile: + type: object + description: "allow tuning reconciling process" + properties: + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + layout: + type: object + description: | + describe current cluster layout, how much shards in cluster, how much replica in shard + allows override settings on each shard and replica separatelly + # nullable: true + properties: + shardsCount: + type: integer + description: | + how much shards for current ClickHouse cluster will run in Kubernetes, + each shard contains shared-nothing part of data and contains set of replicas, + cluster contains 1 shard by default" + replicasCount: + type: integer + description: | + how much replicas in each shards for current cluster will run in Kubernetes, + each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + every shard contains 1 replica by default" + shards: + type: array + description: | + optional, allows override top-level `chi.spec.configuration`, cluster-level + `chi.spec.configuration.clusters` settings for each shard separately, + use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + definitionType: + type: string + description: "DEPRECATED - to be removed soon" + weight: + type: integer + description: | + optional, 1 by default, allows setup shard <weight> setting which will use during insert into tables with `Distributed` engine, + will apply in <remote_servers> inside ConfigMap which will mount in /etc/clickhouse-server/config.d/chop-generated-remote_servers.xml + More details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + internalReplication: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, `true` by default when `chi.spec.configuration.clusters[].layout.ReplicaCount` > 1 and 0 otherwise + allows setup <internal_replication> setting which will use during insert into tables with `Distributed` engine for insert only in one live replica and other replicas will download inserted data during replication, + will apply in <remote_servers> inside ConfigMap which will mount in /etc/clickhouse-server/config.d/chop-generated-remote_servers.xml + More details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one shard during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` and cluster-level `chi.spec.configuration.clusters.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected shard + override top-level `chi.spec.configuration.templates` and cluster-level `chi.spec.configuration.clusters.templates` + replicasCount: + type: integer + description: | + optional, how much replicas in selected shard for selected ClickHouse cluster will run in Kubernetes, each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + shard contains 1 replica by default + override cluster-level `chi.spec.configuration.clusters.layout.replicasCount` + minimum: 1 + replicas: + type: array + description: | + optional, allows override behavior for selected replicas from cluster-level `chi.spec.configuration.clusters` and shard-level `chi.spec.configuration.clusters.layout.shards` + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `tcp` for selected replica, override `chi.spec.templates.hostTemplates.spec.tcpPort` + allows connect to `clickhouse-server` via TCP Native protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `http` for selected replica, override `chi.spec.templates.hostTemplates.spec.httpPort` + allows connect to `clickhouse-server` via HTTP protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `interserver` for selected replica, override `chi.spec.templates.hostTemplates.spec.interserverHTTPPort` + allows connect between replicas inside same shard during fetch replicated data parts HTTP protocol + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and shard-level `chi.spec.configuration.clusters.layout.shards.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files`, cluster-level `chi.spec.configuration.clusters.files` and shard-level `chi.spec.configuration.clusters.layout.shards.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` and shard-level `chi.spec.configuration.clusters.layout.shards.templates` + replicas: + type: array + description: "optional, allows override top-level `chi.spec.configuration` and cluster-level `chi.spec.configuration.clusters` configuration for each replica and each shard relates to selected replica, use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and will ignore if shard-level `chi.spec.configuration.clusters.layout.shards` present + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` + shardsCount: + type: integer + description: "optional, count of shards related to current replica, you can override each shard behavior on low-level `chi.spec.configuration.clusters.layout.replicas.shards`" + minimum: 1 + shards: + type: array + description: "optional, list of shards related to current replica, will ignore if `chi.spec.configuration.clusters.layout.shards` presents" + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `tcp` for selected shard, override `chi.spec.templates.hostTemplates.spec.tcpPort` + allows connect to `clickhouse-server` via TCP Native protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `http` for selected shard, override `chi.spec.templates.hostTemplates.spec.httpPort` + allows connect to `clickhouse-server` via HTTP protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `interserver` for selected shard, override `chi.spec.templates.hostTemplates.spec.interserverHTTPPort` + allows connect between replicas inside same shard during fetch replicated data parts HTTP protocol + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and replica-level `chi.spec.configuration.clusters.layout.replicas.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates` + templates: + type: object + description: "allows define templates which will use for render Kubernetes resources like StatefulSet, ConfigMap, Service, PVC, by default, clickhouse-operator have own templates, but you can override it" + # nullable: true + properties: + hostTemplates: + type: array + description: "hostTemplate will use during apply to generate `clickhose-server` config files" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.hostTemplate`, cluster-level `chi.spec.configuration.clusters.templates.hostTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.hostTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.hostTemplate`" + type: string + portDistribution: + type: array + description: "define how will distribute numeric values of named ports in `Pod.spec.containers.ports` and clickhouse-server configs" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "type of distribution, when `Unspecified` (default value) then all listen ports on clickhouse-server configuration in all Pods will have the same value, when `ClusterScopeIndex` then ports will increment to offset from base value depends on shard and replica index inside cluster with combination of `chi.spec.templates.podTemlates.spec.HostNetwork` it allows setup ClickHouse cluster inside Kubernetes and provide access via external network bypass Kubernetes internal network" + enum: + # List PortDistributionXXX constants + - "" + - "Unspecified" + - "ClusterScopeIndex" + spec: + # Host + type: object + properties: + name: + type: string + description: "by default, hostname will generate, but this allows define custom name for each `clickhouse-server`" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `tcp_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=tcp]` + More info: https://clickhouse.tech/docs/en/interfaces/tcp/ + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `http_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=http]` + More info: https://clickhouse.tech/docs/en/interfaces/http/ + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `interserver_http_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=interserver]` + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#interserver-http-port + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: "be careful, this part of CRD allows override template inside template, don't use it if you don't understand what you do" + podTemplates: + type: array + description: | + podTemplate will use during render `Pod` inside `StatefulSet.spec` and allows define rendered `Pod.spec`, pod scheduling distribution and pod zone + More information: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatespodtemplates + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.podTemplate`, cluster-level `chi.spec.configuration.clusters.templates.podTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.podTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.podTemplate`" + generateName: + type: string + description: "allows define format for generated `Pod` name, look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates for details about available template variables" + zone: + type: object + description: "allows define custom zone name and will separate ClickHouse `Pods` between nodes, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + #required: + # - values + properties: + key: + type: string + description: "optional, if defined, allows select kubernetes nodes by label with `name` equal `key`" + values: + type: array + description: "optional, if defined, allows select kubernetes nodes by label with `value` in `values`" + # nullable: true + items: + type: string + distribution: + type: string + description: "DEPRECATED, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + enum: + - "" + - "Unspecified" + - "OnePerHost" + podDistribution: + type: array + description: "define ClickHouse Pod distribution policy between Kubernetes Nodes inside Shard, Replica, Namespace, CHI, another ClickHouse cluster" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "you can define multiple affinity policy types" + enum: + # List PodDistributionXXX constants + - "" + - "Unspecified" + - "ClickHouseAntiAffinity" + - "ShardAntiAffinity" + - "ReplicaAntiAffinity" + - "AnotherNamespaceAntiAffinity" + - "AnotherClickHouseInstallationAntiAffinity" + - "AnotherClusterAntiAffinity" + - "MaxNumberPerNode" + - "NamespaceAffinity" + - "ClickHouseInstallationAffinity" + - "ClusterAffinity" + - "ShardAffinity" + - "ReplicaAffinity" + - "PreviousTailAffinity" + - "CircularReplication" + scope: + type: string + description: "scope for apply each podDistribution" + enum: + # list PodDistributionScopeXXX constants + - "" + - "Unspecified" + - "Shard" + - "Replica" + - "Cluster" + - "ClickHouseInstallation" + - "Namespace" + number: + type: integer + description: "define, how much ClickHouse Pods could be inside selected scope with selected distribution type" + minimum: 0 + maximum: 65535 + topologyKey: + type: string + description: | + use for inter-pod affinity look to `pod.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution.podAffinityTerm.topologyKey`, + more info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity" + metadata: + type: object + description: | + allows pass standard object's metadata from template to Pod + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify PodSpec + type: object + description: "allows define whole Pod.spec inside StaefulSet.spec, look to https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates for details" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + volumeClaimTemplates: + type: array + description: | + allows define template for rendering `PVC` kubernetes resource, which would use inside `Pod` for mount clickhouse `data`, clickhouse `logs` or something else + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + top-level `chi.spec.defaults.templates.dataVolumeClaimTemplate` or `chi.spec.defaults.templates.logVolumeClaimTemplate`, + cluster-level `chi.spec.configuration.clusters.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.templates.logVolumeClaimTemplate`, + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.shards.temlates.logVolumeClaimTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.replicas.templates.logVolumeClaimTemplate` + provisioner: + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + metadata: + type: object + description: | + allows to pass standard object's metadata from template to PVC + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + description: | + allows define all aspects of `PVC` resource + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims + # nullable: true + x-kubernetes-preserve-unknown-fields: true + serviceTemplates: + type: array + description: | + allows define template for rendering `Service` which would get endpoint from Pods which scoped chi-wide, cluster-wide, shard-wide, replica-wide level + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + chi-level `chi.spec.defaults.templates.serviceTemplate` + cluster-level `chi.spec.configuration.clusters.templates.clusterServiceTemplate` + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.shardServiceTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.replicaServiceTemplate` or `chi.spec.configuration.clusters.layout.shards.replicas.replicaServiceTemplate` + generateName: + type: string + description: | + allows define format for generated `Service` name, + look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates + for details about available template variables" + metadata: + # TODO specify ObjectMeta + type: object + description: | + allows pass standard object's metadata from template to Service + Could be use for define specificly for Cloud Provider metadata which impact to behavior of service + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify ServiceSpec + type: object + description: | + describe behavior of generated Service + More info: https://kubernetes.io/docs/concepts/services-networking/service/ + # nullable: true + x-kubernetes-preserve-unknown-fields: true + useTemplates: + type: array + description: | + list of `ClickHouseInstallationTemplate` (chit) resource names which will merge with current `CHI` + manifest during render Kubernetes resources to create related ClickHouse clusters" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "name of `ClickHouseInstallationTemplate` (chit) resource" + namespace: + type: string + description: "Kubernetes namespace where need search `chit` resource, depending on `watchNamespaces` settings in `clickhouse-operator`" + useType: + type: string + description: "optional, current strategy is only merge, and current `chi` settings have more priority than merged template `chit`" + enum: + # List useTypeXXX constants from model + - "" + - "merge" +--- +# Template Parameters: +# +# KIND=ClickHouseInstallationTemplate +# SINGULAR=clickhouseinstallationtemplate +# PLURAL=clickhouseinstallationtemplates +# SHORT=chit +# OPERATOR_VERSION=0.26.3 +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clickhouseinstallationtemplates.clickhouse.altinity.com + labels: + clickhouse.altinity.com/chop: 0.26.3 +spec: + group: clickhouse.altinity.com + scope: Namespaced + names: + kind: ClickHouseInstallationTemplate + singular: clickhouseinstallationtemplate + plural: clickhouseinstallationtemplates + shortNames: + - chit + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: status + type: string + description: Resource status + jsonPath: .status.status + - name: version + type: string + description: Operator version + priority: 1 # show in wide view + jsonPath: .status.chop-version + - name: clusters + type: integer + description: Clusters count + jsonPath: .status.clusters + - name: shards + type: integer + description: Shards count + priority: 1 # show in wide view + jsonPath: .status.shards + - name: hosts + type: integer + description: Hosts count + jsonPath: .status.hosts + - name: taskID + type: string + description: TaskID + priority: 1 # show in wide view + jsonPath: .status.taskID + - name: hosts-completed + type: integer + description: Completed hosts count + jsonPath: .status.hostsCompleted + - name: hosts-updated + type: integer + description: Updated hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsUpdated + - name: hosts-added + type: integer + description: Added hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsAdded + - name: hosts-deleted + type: integer + description: Hosts deleted count + priority: 1 # show in wide view + jsonPath: .status.hostsDeleted + - name: endpoint + type: string + description: Client access endpoint + priority: 1 # show in wide view + jsonPath: .status.endpoint + - name: age + type: date + description: Age of the resource + # Displayed in all priorities + jsonPath: .metadata.creationTimestamp + - name: suspend + type: string + description: Suspend reconciliation + # Displayed in all priorities + jsonPath: .spec.suspend + subresources: + status: {} + schema: + openAPIV3Schema: + description: "define a set of Kubernetes resources (StatefulSet, PVC, Service, ConfigMap) which describe behavior one or more clusters" + type: object + required: + - spec + properties: + apiVersion: + description: | + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: | + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + type: object + description: | + Status contains many fields like a normalized configuration, clickhouse-operator version, current action and all applied action list, current taskID and all applied taskIDs and other + properties: + chop-version: + type: string + description: "Operator version" + chop-commit: + type: string + description: "Operator git commit SHA" + chop-date: + type: string + description: "Operator build date" + chop-ip: + type: string + description: "IP address of the operator's pod which managed this resource" + clusters: + type: integer + minimum: 0 + description: "Clusters count" + shards: + type: integer + minimum: 0 + description: "Shards count" + replicas: + type: integer + minimum: 0 + description: "Replicas count" + hosts: + type: integer + minimum: 0 + description: "Hosts count" + status: + type: string + description: "Status" + taskID: + type: string + description: "Current task id" + taskIDsStarted: + type: array + description: "Started task ids" + nullable: true + items: + type: string + taskIDsCompleted: + type: array + description: "Completed task ids" + nullable: true + items: + type: string + action: + type: string + description: "Action" + actions: + type: array + description: "Actions" + nullable: true + items: + type: string + error: + type: string + description: "Last error" + errors: + type: array + description: "Errors" + nullable: true + items: + type: string + hostsUnchanged: + type: integer + minimum: 0 + description: "Unchanged Hosts count" + hostsUpdated: + type: integer + minimum: 0 + description: "Updated Hosts count" + hostsAdded: + type: integer + minimum: 0 + description: "Added Hosts count" + hostsCompleted: + type: integer + minimum: 0 + description: "Completed Hosts count" + hostsDeleted: + type: integer + minimum: 0 + description: "Deleted Hosts count" + hostsDelete: + type: integer + minimum: 0 + description: "About to delete Hosts count" + pods: + type: array + description: "Pods" + nullable: true + items: + type: string + pod-ips: + type: array + description: "Pod IPs" + nullable: true + items: + type: string + fqdns: + type: array + description: "Pods FQDNs" + nullable: true + items: + type: string + endpoint: + type: string + description: "Endpoint" + endpoints: + type: array + description: "All endpoints" + nullable: true + items: + type: string + generation: + type: integer + minimum: 0 + description: "Generation" + normalized: + type: object + description: "Normalized resource requested" + nullable: true + x-kubernetes-preserve-unknown-fields: true + normalizedCompleted: + type: object + description: "Normalized resource completed" + nullable: true + x-kubernetes-preserve-unknown-fields: true + actionPlan: + type: object + description: "Action Plan" + nullable: true + x-kubernetes-preserve-unknown-fields: true + hostsWithTablesCreated: + type: array + description: "List of hosts with tables created by the operator" + nullable: true + items: + type: string + hostsWithReplicaCaughtUp: + type: array + description: "List of hosts with replica caught up" + nullable: true + items: + type: string + usedTemplates: + type: array + description: "List of templates used to build this CHI" + nullable: true + x-kubernetes-preserve-unknown-fields: true + items: + type: object + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + # x-kubernetes-preserve-unknown-fields: true + description: | + Specification of the desired behavior of one or more ClickHouse clusters + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md + properties: + taskID: + type: string + description: | + Allows to define custom taskID for CHI update and watch status of this update execution. + Displayed in all .status.taskID* fields. + By default (if not filled) every update of CHI manifest will generate random taskID + stop: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + restart: + type: string + description: | + In case 'RollingUpdate' specified, the operator will always restart ClickHouse pods during reconcile. + This options is used in rare cases when force restart is required and is typically removed after the use in order to avoid unneeded restarts. + enum: + - "" + - "RollingUpdate" + suspend: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Suspend reconciliation of resources managed by a ClickHouse Installation. + Works as the following: + - When `suspend` is `true` operator stops reconciling all resources. + - When `suspend` is `false` or not set, operator reconciles all resources. + troubleshoot: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Allows to troubleshoot Pods during CrashLoopBack state. + This may happen when wrong configuration applied, in this case `clickhouse-server` wouldn't start. + Command within ClickHouse container is modified with `sleep` in order to avoid quick restarts + and give time to troubleshoot via CLI. + Liveness and Readiness probes are disabled as well. + namespaceDomainPattern: + type: string + description: | + Custom domain pattern which will be used for DNS names of `Service` or `Pod`. + Typical use scenario - custom cluster domain in Kubernetes cluster + Example: %s.svc.my.test + templating: + type: object + # nullable: true + description: | + Optional, applicable inside ClickHouseInstallationTemplate only. + Defines current ClickHouseInstallationTemplate application options to target ClickHouseInstallation(s)." + properties: + policy: + type: string + description: | + When defined as `auto` inside ClickhouseInstallationTemplate, this ClickhouseInstallationTemplate + will be auto-added into ClickHouseInstallation, selectable by `chiSelector`. + Default value is `manual`, meaning ClickHouseInstallation should request this ClickhouseInstallationTemplate explicitly. + enum: + - "" + - "auto" + - "manual" + chiSelector: + type: object + description: "Optional, defines selector for ClickHouseInstallation(s) to be templated with ClickhouseInstallationTemplate" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + reconciling: + type: object + description: "[OBSOLETED] Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + # nullable: true + properties: + policy: + type: string + description: | + DISCUSSED TO BE DEPRECATED + Syntax sugar + Overrides all three 'reconcile.host.wait.{exclude, queries, include}' values from the operator's config + Possible values: + - wait - should wait to exclude host, complete queries and include host back into the cluster + - nowait - should NOT wait to exclude host, complete queries and include host back into the cluster + enum: + - "" + - "wait" + - "nowait" + configMapPropagationTimeout: + type: integer + description: | + Timeout in seconds for `clickhouse-operator` to wait for modified `ConfigMap` to propagate into the `Pod` + More details: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically + minimum: 0 + maximum: 3600 + cleanup: + type: object + description: "Optional, defines behavior for cleanup Kubernetes resources during reconcile cycle" + # nullable: true + properties: + unknownObjects: + type: object + description: | + Describes what clickhouse-operator should do with found Kubernetes resources which should be managed by clickhouse-operator, + but do not have `ownerReference` to any currently managed `ClickHouseInstallation` resource. + Default behavior is `Delete`" + # nullable: true + properties: + statefulSet: + type: string + description: "Behavior policy for unknown StatefulSet, `Delete` by default" + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown PVC, `Delete` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown ConfigMap, `Delete` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown Service, `Delete` by default" + reconcileFailedObjects: + type: object + description: | + Describes what clickhouse-operator should do with Kubernetes resources which are failed during reconcile. + Default behavior is `Retain`" + # nullable: true + properties: + statefulSet: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed StatefulSet, `Retain` by default" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed PVC, `Retain` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed ConfigMap, `Retain` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed Service, `Retain` by default" + macros: + type: object + description: "macros parameters" + properties: + sections: + type: object + description: "sections behaviour for macros" + properties: + users: + type: object + description: "sections behaviour for macros on users" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + profiles: + type: object + description: "sections behaviour for macros on profiles" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + quotas: + type: object + description: "sections behaviour for macros on quotas" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + settings: + type: object + description: "sections behaviour for macros on settings" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + files: + type: object + description: "sections behaviour for macros on files" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + statefulSet: + type: object + description: "Optional, StatefulSet reconcile behavior tuning" + properties: + create: + type: object + description: "Behavior during create StatefulSet" + properties: + onFailure: + type: string + description: | + What to do in case created StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. delete - delete newly created problematic StatefulSet and follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "delete" + - "ignore" + update: + type: object + description: "Behavior during update StatefulSet" + properties: + timeout: + type: integer + description: "How many seconds to wait for StatefulSet to be 'Ready' during update" + minimum: 0 + maximum: 3600 + pollInterval: + type: integer + description: "How many seconds to wait between checks for StatefulSet status during update" + minimum: 1 + maximum: 600 + onFailure: + type: string + description: | + What to do in case updated StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. rollback - delete Pod and rollback StatefulSet to previous Generation. Follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "rollback" + - "ignore" + recreate: + type: object + description: "Behavior during recreate StatefulSet" + properties: + onDataLoss: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to PVC data loss or missing volumes. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + onUpdateFailure: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to update failure or StatefulSet not ready. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + reconcile: + type: object + # nullable: true + properties: + policy: + type: string + description: | + DISCUSSED TO BE DEPRECATED + Syntax sugar + Overrides all three 'reconcile.host.wait.{exclude, queries, include}' values from the operator's config + Possible values: + - wait - should wait to exclude host, complete queries and include host back into the cluster + - nowait - should NOT wait to exclude host, complete queries and include host back into the cluster + enum: + - "" + - "wait" + - "nowait" + configMapPropagationTimeout: + type: integer + description: | + Timeout in seconds for `clickhouse-operator` to wait for modified `ConfigMap` to propagate into the `Pod` + More details: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically + minimum: 0 + maximum: 3600 + cleanup: + type: object + description: "Optional, defines behavior for cleanup Kubernetes resources during reconcile cycle" + # nullable: true + properties: + unknownObjects: + type: object + description: | + Describes what clickhouse-operator should do with found Kubernetes resources which should be managed by clickhouse-operator, + but do not have `ownerReference` to any currently managed `ClickHouseInstallation` resource. + Default behavior is `Delete`" + # nullable: true + properties: + statefulSet: + type: string + description: "Behavior policy for unknown StatefulSet, `Delete` by default" + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown PVC, `Delete` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown ConfigMap, `Delete` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown Service, `Delete` by default" + reconcileFailedObjects: + type: object + description: | + Describes what clickhouse-operator should do with Kubernetes resources which are failed during reconcile. + Default behavior is `Retain`" + # nullable: true + properties: + statefulSet: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed StatefulSet, `Retain` by default" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed PVC, `Retain` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed ConfigMap, `Retain` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed Service, `Retain` by default" + macros: + type: object + description: "macros parameters" + properties: + sections: + type: object + description: "sections behaviour for macros" + properties: + users: + type: object + description: "sections behaviour for macros on users" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + profiles: + type: object + description: "sections behaviour for macros on profiles" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + quotas: + type: object + description: "sections behaviour for macros on quotas" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + settings: + type: object + description: "sections behaviour for macros on settings" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + files: + type: object + description: "sections behaviour for macros on files" + properties: + enabled: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "enabled or not" + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + statefulSet: + type: object + description: "Optional, StatefulSet reconcile behavior tuning" + properties: + create: + type: object + description: "Behavior during create StatefulSet" + properties: + onFailure: + type: string + description: | + What to do in case created StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. delete - delete newly created problematic StatefulSet and follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "delete" + - "ignore" + update: + type: object + description: "Behavior during update StatefulSet" + properties: + timeout: + type: integer + description: "How many seconds to wait for StatefulSet to be 'Ready' during update" + minimum: 0 + maximum: 3600 + pollInterval: + type: integer + description: "How many seconds to wait between checks for StatefulSet status during update" + minimum: 1 + maximum: 600 + onFailure: + type: string + description: | + What to do in case updated StatefulSet is not in 'Ready' after `reconcile.statefulSet.update.timeout` seconds. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet, leave it as it is. + 2. rollback - delete Pod and rollback StatefulSet to previous Generation. Follow 'abort' path afterwards. + 3. ignore - ignore an error, pretend nothing happened, continue reconcile and move on to the next StatefulSet. + enum: + - "" + - "abort" + - "rollback" + - "ignore" + recreate: + type: object + description: "Behavior during recreate StatefulSet" + properties: + onDataLoss: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to PVC data loss or missing volumes. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + onUpdateFailure: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to update failure or StatefulSet not ready. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate - proceed and recreate StatefulSet. + enum: + - "" + - "abort" + - "recreate" + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + description: "Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + defaults: + type: object + description: | + define default behavior for whole ClickHouseInstallation, some behavior can be re-define on cluster, shard and replica level + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specdefaults + # nullable: true + properties: + replicasUseFQDN: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + define should replicas be specified by FQDN in `<host></host>`. + In case of "no" will use short hostname and clickhouse-server will use kubernetes default suffixes for DNS lookup + "no" by default + distributedDDL: + type: object + description: | + allows change `<yandex><distributed_ddl></distributed_ddl></yandex>` settings + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings-distributed_ddl + # nullable: true + properties: + profile: + type: string + description: "Settings from this profile will be used to execute DDL queries" + storageManagement: + type: object + description: default storage management options + properties: + provisioner: + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + templates: + type: object + description: "optional, configuration of the templates names which will use for generate Kubernetes resources according to one or more ClickHouse clusters described in current ClickHouseInstallation (chi) resource" + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + configuration: + type: object + description: "allows configure multiple aspects and behavior for `clickhouse-server` instance and also allows describe multiple `clickhouse-server` clusters inside one `chi` resource" + # nullable: true + properties: + zookeeper: + type: object + description: | + allows configure <yandex><zookeeper>..</zookeeper></yandex> section in each `Pod` during generate `ConfigMap` which will mounted in `/etc/clickhouse-server/config.d/` + `clickhouse-operator` itself doesn't manage Zookeeper, please install Zookeeper separatelly look examples on https://github.com/Altinity/clickhouse-operator/tree/master/deploy/zookeeper/ + currently, zookeeper (or clickhouse-keeper replacement) used for *ReplicatedMergeTree table engines and for `distributed_ddl` + More details: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings_zookeeper + # nullable: true + properties: + nodes: + type: array + description: "describe every available zookeeper cluster node for interaction" + # nullable: true + items: + type: object + #required: + # - host + properties: + host: + type: string + description: "dns name or ip address for Zookeeper node" + port: + type: integer + description: "TCP port which used to connect to Zookeeper node" + minimum: 0 + maximum: 65535 + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "if a secure connection to Zookeeper is required" + availabilityZone: + type: string + description: "availability zone for Zookeeper node" + session_timeout_ms: + type: integer + description: "session timeout during connect to Zookeeper" + operation_timeout_ms: + type: integer + description: "one operation timeout during Zookeeper transactions" + root: + type: string + description: "optional root znode path inside zookeeper to store ClickHouse related data (replication queue or distributed DDL)" + identity: + type: string + description: "optional access credentials string with `user:password` format used when use digest authorization in Zookeeper" + use_compression: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Enables compression in Keeper protocol if set to true" + users: + type: object + description: | + allows configure <yandex><users>..</users></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure password hashed, authorization restrictions, database level security row filters etc. + More details: https://clickhouse.tech/docs/en/operations/settings/settings-users/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationusers + + any key could contains `valueFrom` with `secretKeyRef` which allow pass password from kubernetes secrets + secret value will pass in `pod.spec.containers.evn`, and generate with from_env=XXX in XML in /etc/clickhouse-server/users.d/chop-generated-users.xml + it not allow automatically updates when updates `secret`, change spec.taskID for manually trigger reconcile cycle + + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + any key with prefix `k8s_secret_` shall has value with format namespace/secret/key or secret/key + in this case value from secret will write directly into XML tag during render *-usersd ConfigMap + + any key with prefix `k8s_secret_env` shall has value with format namespace/secret/key or secret/key + in this case value from secret will write into environment variable and write to XML tag via from_env=XXX + + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + # nullable: true + x-kubernetes-preserve-unknown-fields: true + profiles: + type: object + description: | + allows configure <yandex><profiles>..</profiles></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure any aspect of settings profile + More details: https://clickhouse.tech/docs/en/operations/settings/settings-profiles/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationprofiles + # nullable: true + x-kubernetes-preserve-unknown-fields: true + quotas: + type: object + description: | + allows configure <yandex><quotas>..</quotas></yandex> section in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/users.d/` + you can configure any aspect of resource quotas + More details: https://clickhouse.tech/docs/en/operations/quotas/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationquotas + # nullable: true + x-kubernetes-preserve-unknown-fields: true + settings: + type: object + description: | + allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + Your yaml code will convert to XML, see examples https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specconfigurationsettings + + any key could contains `valueFrom` with `secretKeyRef` which allow pass password from kubernetes secrets + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + + secret value will pass in `pod.spec.env`, and generate with from_env=XXX in XML in /etc/clickhouse-server/config.d/chop-generated-settings.xml + it not allow automatically updates when updates `secret`, change spec.taskID for manually trigger reconcile cycle + # nullable: true + x-kubernetes-preserve-unknown-fields: true + files: + type: object + description: | + allows define content of any setting file inside each `Pod` during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + every key in this object is the file name + every value in this object is the file content + you can use `!!binary |` and base64 for binary files, see details here https://yaml.org/type/binary.html + each key could contains prefix like {common}, {users}, {hosts} or config.d, users.d, conf.d, wrong prefixes will be ignored, subfolders also will be ignored + More details: https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-05-files-nested.yaml + + any key could contains `valueFrom` with `secretKeyRef` which allow pass values from kubernetes secrets + secrets will mounted into pod as separate volume in /etc/clickhouse-server/secrets.d/ + and will automatically update when update secret + it useful for pass SSL certificates from cert-manager or similar tool + look into https://github.com/Altinity/clickhouse-operator/blob/master/docs/chi-examples/05-settings-01-overview.yaml for examples + # nullable: true + x-kubernetes-preserve-unknown-fields: true + clusters: + type: array + description: | + describes clusters layout and allows change settings on cluster-level, shard-level and replica-level + every cluster is a set of StatefulSet, one StatefulSet contains only one Pod with `clickhouse-server` + all Pods will rendered in <remote_server> part of ClickHouse configs, mounted from ConfigMap as `/etc/clickhouse-server/config.d/chop-generated-remote_servers.xml` + Clusters will use for Distributed table engine, more details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + If `cluster` contains zookeeper settings (could be inherited from top `chi` level), when you can create *ReplicatedMergeTree tables + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "cluster name, used to identify set of servers and wide used during generate names of related Kubernetes resources" + minLength: 1 + # See namePartClusterMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + zookeeper: + type: object + # nullable: true + properties: + nodes: + type: array + description: "describe every available zookeeper cluster node for interaction" + # nullable: true + items: + type: object + #required: + # - host + properties: + host: + type: string + description: "dns name or ip address for Zookeeper node" + port: + type: integer + description: "TCP port which used to connect to Zookeeper node" + minimum: 0 + maximum: 65535 + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "if a secure connection to Zookeeper is required" + availabilityZone: + type: string + description: "availability zone for Zookeeper node" + session_timeout_ms: + type: integer + description: "session timeout during connect to Zookeeper" + operation_timeout_ms: + type: integer + description: "one operation timeout during Zookeeper transactions" + root: + type: string + description: "optional root znode path inside zookeeper to store ClickHouse related data (replication queue or distributed DDL)" + identity: + type: string + description: "optional access credentials string with `user:password` format used when use digest authorization in Zookeeper" + use_compression: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Enables compression in Keeper protocol if set to true" + description: | + optional, allows configure <yandex><zookeeper>..</zookeeper></yandex> section in each `Pod` only in current ClickHouse cluster, during generate `ConfigMap` which will mounted in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.zookeeper` settings + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` on current cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected cluster + override top-level `chi.spec.configuration.templates` + schemaPolicy: + type: object + description: | + describes how schema is propagated within replicas and shards + properties: + replica: + type: string + description: "how schema is propagated within a replica" + enum: + # List SchemaPolicyReplicaXXX constants from model + - "" + - "None" + - "All" + shard: + type: string + description: "how schema is propagated between shards" + enum: + # List SchemaPolicyShardXXX constants from model + - "" + - "None" + - "All" + - "DistributedTablesOnly" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: optional, open secure ports for cluster + secret: + type: object + description: "optional, shared secret value to secure cluster communications" + properties: + auto: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Auto-generate shared secret value to secure cluster communications" + value: + description: "Cluster shared secret value in plain text" + type: string + valueFrom: + description: "Cluster shared secret source" + type: object + properties: + secretKeyRef: + description: | + Selects a key of a secret in the clickhouse installation namespace. + Should not be used if value is not empty. + type: object + properties: + name: + description: | + Name of the referent. More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + key: + description: The key of the secret to select from. Must be a valid secret key. + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - name + - key + pdbManaged: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Specifies whether the Pod Disruption Budget (PDB) should be managed. + During the next installation, if PDB management is enabled, the operator will + attempt to retrieve any existing PDB. If none is found, it will create a new one + and initiate a reconciliation loop. If PDB management is disabled, the existing PDB + will remain intact, and the reconciliation loop will not be executed. By default, + PDB management is enabled. + pdbMaxUnavailable: + type: integer + description: | + Pod eviction is allowed if at most "pdbMaxUnavailable" pods are unavailable after the eviction, + i.e. even in absence of the evicted pod. For example, one can prevent all voluntary evictions + by specifying 0. This is a mutually exclusive setting with "minAvailable". + minimum: 0 + maximum: 65535 + reconcile: + type: object + description: "allow tuning reconciling process" + properties: + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "The maximum number of cluster shards that may be reconciled in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: | + Allows to stop all ClickHouse clusters defined in a CHI. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for ready probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + layout: + type: object + description: | + describe current cluster layout, how much shards in cluster, how much replica in shard + allows override settings on each shard and replica separatelly + # nullable: true + properties: + shardsCount: + type: integer + description: | + how much shards for current ClickHouse cluster will run in Kubernetes, + each shard contains shared-nothing part of data and contains set of replicas, + cluster contains 1 shard by default" + replicasCount: + type: integer + description: | + how much replicas in each shards for current cluster will run in Kubernetes, + each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + every shard contains 1 replica by default" + shards: + type: array + description: | + optional, allows override top-level `chi.spec.configuration`, cluster-level + `chi.spec.configuration.clusters` settings for each shard separately, + use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + definitionType: + type: string + description: "DEPRECATED - to be removed soon" + weight: + type: integer + description: | + optional, 1 by default, allows setup shard <weight> setting which will use during insert into tables with `Distributed` engine, + will apply in <remote_servers> inside ConfigMap which will mount in /etc/clickhouse-server/config.d/chop-generated-remote_servers.xml + More details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + internalReplication: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, `true` by default when `chi.spec.configuration.clusters[].layout.ReplicaCount` > 1 and 0 otherwise + allows setup <internal_replication> setting which will use during insert into tables with `Distributed` engine for insert only in one live replica and other replicas will download inserted data during replication, + will apply in <remote_servers> inside ConfigMap which will mount in /etc/clickhouse-server/config.d/chop-generated-remote_servers.xml + More details: https://clickhouse.tech/docs/en/engines/table-engines/special/distributed/ + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one shard during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` and cluster-level `chi.spec.configuration.clusters.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected shard + override top-level `chi.spec.configuration.templates` and cluster-level `chi.spec.configuration.clusters.templates` + replicasCount: + type: integer + description: | + optional, how much replicas in selected shard for selected ClickHouse cluster will run in Kubernetes, each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + shard contains 1 replica by default + override cluster-level `chi.spec.configuration.clusters.layout.replicasCount` + minimum: 1 + replicas: + type: array + description: | + optional, allows override behavior for selected replicas from cluster-level `chi.spec.configuration.clusters` and shard-level `chi.spec.configuration.clusters.layout.shards` + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `tcp` for selected replica, override `chi.spec.templates.hostTemplates.spec.tcpPort` + allows connect to `clickhouse-server` via TCP Native protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `http` for selected replica, override `chi.spec.templates.hostTemplates.spec.httpPort` + allows connect to `clickhouse-server` via HTTP protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `interserver` for selected replica, override `chi.spec.templates.hostTemplates.spec.interserverHTTPPort` + allows connect between replicas inside same shard during fetch replicated data parts HTTP protocol + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and shard-level `chi.spec.configuration.clusters.layout.shards.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files`, cluster-level `chi.spec.configuration.clusters.files` and shard-level `chi.spec.configuration.clusters.layout.shards.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` and shard-level `chi.spec.configuration.clusters.layout.shards.templates` + replicas: + type: array + description: "optional, allows override top-level `chi.spec.configuration` and cluster-level `chi.spec.configuration.clusters` configuration for each replica and each shard relates to selected replica, use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and will ignore if shard-level `chi.spec.configuration.clusters.layout.shards` present + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` + shardsCount: + type: integer + description: "optional, count of shards related to current replica, you can override each shard behavior on low-level `chi.spec.configuration.clusters.layout.replicas.shards`" + minimum: 1 + shards: + type: array + description: "optional, list of shards related to current replica, will ignore if `chi.spec.configuration.clusters.layout.shards` presents" + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `tcp` for selected shard, override `chi.spec.templates.hostTemplates.spec.tcpPort` + allows connect to `clickhouse-server` via TCP Native protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `http` for selected shard, override `chi.spec.templates.hostTemplates.spec.httpPort` + allows connect to `clickhouse-server` via HTTP protocol via kubernetes `Service` + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `Pod.spec.containers.ports` with name `interserver` for selected shard, override `chi.spec.templates.hostTemplates.spec.interserverHTTPPort` + allows connect between replicas inside same shard during fetch replicated data parts HTTP protocol + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and replica-level `chi.spec.configuration.clusters.layout.replicas.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates` + templates: + type: object + description: "allows define templates which will use for render Kubernetes resources like StatefulSet, ConfigMap, Service, PVC, by default, clickhouse-operator have own templates, but you can override it" + # nullable: true + properties: + hostTemplates: + type: array + description: "hostTemplate will use during apply to generate `clickhose-server` config files" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.hostTemplate`, cluster-level `chi.spec.configuration.clusters.templates.hostTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.hostTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.hostTemplate`" + type: string + portDistribution: + type: array + description: "define how will distribute numeric values of named ports in `Pod.spec.containers.ports` and clickhouse-server configs" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "type of distribution, when `Unspecified` (default value) then all listen ports on clickhouse-server configuration in all Pods will have the same value, when `ClusterScopeIndex` then ports will increment to offset from base value depends on shard and replica index inside cluster with combination of `chi.spec.templates.podTemlates.spec.HostNetwork` it allows setup ClickHouse cluster inside Kubernetes and provide access via external network bypass Kubernetes internal network" + enum: + # List PortDistributionXXX constants + - "" + - "Unspecified" + - "ClusterScopeIndex" + spec: + # Host + type: object + properties: + name: + type: string + description: "by default, hostname will generate, but this allows define custom name for each `clickhouse-server`" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + insecure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open insecure ports for cluster, defaults to "yes" + secure: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + optional, open secure ports + tcpPort: + type: integer + description: | + optional, setup `tcp_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=tcp]` + More info: https://clickhouse.tech/docs/en/interfaces/tcp/ + minimum: 1 + maximum: 65535 + tlsPort: + type: integer + minimum: 1 + maximum: 65535 + httpPort: + type: integer + description: | + optional, setup `http_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=http]` + More info: https://clickhouse.tech/docs/en/interfaces/http/ + minimum: 1 + maximum: 65535 + httpsPort: + type: integer + minimum: 1 + maximum: 65535 + interserverHTTPPort: + type: integer + description: | + optional, setup `interserver_http_port` inside `clickhouse-server` settings for each Pod where current template will apply + if specified, should have equal value with `chi.spec.templates.podTemplates.spec.containers.ports[name=interserver]` + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#interserver-http-port + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: "be careful, this part of CRD allows override template inside template, don't use it if you don't understand what you do" + podTemplates: + type: array + description: | + podTemplate will use during render `Pod` inside `StatefulSet.spec` and allows define rendered `Pod.spec`, pod scheduling distribution and pod zone + More information: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatespodtemplates + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.podTemplate`, cluster-level `chi.spec.configuration.clusters.templates.podTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.podTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.podTemplate`" + generateName: + type: string + description: "allows define format for generated `Pod` name, look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates for details about available template variables" + zone: + type: object + description: "allows define custom zone name and will separate ClickHouse `Pods` between nodes, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + #required: + # - values + properties: + key: + type: string + description: "optional, if defined, allows select kubernetes nodes by label with `name` equal `key`" + values: + type: array + description: "optional, if defined, allows select kubernetes nodes by label with `value` in `values`" + # nullable: true + items: + type: string + distribution: + type: string + description: "DEPRECATED, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + enum: + - "" + - "Unspecified" + - "OnePerHost" + podDistribution: + type: array + description: "define ClickHouse Pod distribution policy between Kubernetes Nodes inside Shard, Replica, Namespace, CHI, another ClickHouse cluster" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "you can define multiple affinity policy types" + enum: + # List PodDistributionXXX constants + - "" + - "Unspecified" + - "ClickHouseAntiAffinity" + - "ShardAntiAffinity" + - "ReplicaAntiAffinity" + - "AnotherNamespaceAntiAffinity" + - "AnotherClickHouseInstallationAntiAffinity" + - "AnotherClusterAntiAffinity" + - "MaxNumberPerNode" + - "NamespaceAffinity" + - "ClickHouseInstallationAffinity" + - "ClusterAffinity" + - "ShardAffinity" + - "ReplicaAffinity" + - "PreviousTailAffinity" + - "CircularReplication" + scope: + type: string + description: "scope for apply each podDistribution" + enum: + # list PodDistributionScopeXXX constants + - "" + - "Unspecified" + - "Shard" + - "Replica" + - "Cluster" + - "ClickHouseInstallation" + - "Namespace" + number: + type: integer + description: "define, how much ClickHouse Pods could be inside selected scope with selected distribution type" + minimum: 0 + maximum: 65535 + topologyKey: + type: string + description: | + use for inter-pod affinity look to `pod.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution.podAffinityTerm.topologyKey`, + more info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity" + metadata: + type: object + description: | + allows pass standard object's metadata from template to Pod + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify PodSpec + type: object + description: "allows define whole Pod.spec inside StaefulSet.spec, look to https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates for details" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + volumeClaimTemplates: + type: array + description: | + allows define template for rendering `PVC` kubernetes resource, which would use inside `Pod` for mount clickhouse `data`, clickhouse `logs` or something else + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + top-level `chi.spec.defaults.templates.dataVolumeClaimTemplate` or `chi.spec.defaults.templates.logVolumeClaimTemplate`, + cluster-level `chi.spec.configuration.clusters.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.templates.logVolumeClaimTemplate`, + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.shards.temlates.logVolumeClaimTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.replicas.templates.logVolumeClaimTemplate` + provisioner: + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + metadata: + type: object + description: | + allows to pass standard object's metadata from template to PVC + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + description: | + allows define all aspects of `PVC` resource + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims + # nullable: true + x-kubernetes-preserve-unknown-fields: true + serviceTemplates: + type: array + description: | + allows define template for rendering `Service` which would get endpoint from Pods which scoped chi-wide, cluster-wide, shard-wide, replica-wide level + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + chi-level `chi.spec.defaults.templates.serviceTemplate` + cluster-level `chi.spec.configuration.clusters.templates.clusterServiceTemplate` + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.shardServiceTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.replicaServiceTemplate` or `chi.spec.configuration.clusters.layout.shards.replicas.replicaServiceTemplate` + generateName: + type: string + description: | + allows define format for generated `Service` name, + look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates + for details about available template variables" + metadata: + # TODO specify ObjectMeta + type: object + description: | + allows pass standard object's metadata from template to Service + Could be use for define specificly for Cloud Provider metadata which impact to behavior of service + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify ServiceSpec + type: object + description: | + describe behavior of generated Service + More info: https://kubernetes.io/docs/concepts/services-networking/service/ + # nullable: true + x-kubernetes-preserve-unknown-fields: true + useTemplates: + type: array + description: | + list of `ClickHouseInstallationTemplate` (chit) resource names which will merge with current `CHI` + manifest during render Kubernetes resources to create related ClickHouse clusters" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "name of `ClickHouseInstallationTemplate` (chit) resource" + namespace: + type: string + description: "Kubernetes namespace where need search `chit` resource, depending on `watchNamespaces` settings in `clickhouse-operator`" + useType: + type: string + description: "optional, current strategy is only merge, and current `chi` settings have more priority than merged template `chit`" + enum: + # List useTypeXXX constants from model + - "" + - "merge" +--- +# Template Parameters: +# +# NONE +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clickhouseoperatorconfigurations.clickhouse.altinity.com + labels: + clickhouse.altinity.com/chop: 0.26.3 +spec: + group: clickhouse.altinity.com + scope: Namespaced + names: + kind: ClickHouseOperatorConfiguration + singular: clickhouseoperatorconfiguration + plural: clickhouseoperatorconfigurations + shortNames: + - chopconf + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: namespaces + type: string + description: Watch namespaces + jsonPath: .status + - name: age + type: date + description: Age of the resource + # Displayed in all priorities + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + description: "allows customize `clickhouse-operator` settings, need restart clickhouse-operator pod after adding, more details https://github.com/Altinity/clickhouse-operator/blob/master/docs/operator_configuration.md" + x-kubernetes-preserve-unknown-fields: true + properties: + status: + type: object + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + description: | + Allows to define settings of the clickhouse-operator. + More info: https://github.com/Altinity/clickhouse-operator/blob/master/config/config.yaml + Check into etc-clickhouse-operator* ConfigMaps if you need more control + x-kubernetes-preserve-unknown-fields: true + properties: + watch: + type: object + description: "Parameters for watch kubernetes resources which used by clickhouse-operator deployment" + properties: + namespaces: + type: object + description: "List of namespaces where clickhouse-operator watches for events." + x-kubernetes-preserve-unknown-fields: true + clickhouse: + type: object + description: "Clickhouse related parameters used by clickhouse-operator" + properties: + configuration: + type: object + properties: + file: + type: object + properties: + path: + type: object + description: | + Each 'path' can be either absolute or relative. + In case path is absolute - it is used as is. + In case path is relative - it is relative to the folder where configuration file you are reading right now is located. + properties: + common: + type: string + description: | + Path to the folder where ClickHouse configuration files common for all instances within a CHI are located. + Default value - config.d + host: + type: string + description: | + Path to the folder where ClickHouse configuration files unique for each instance (host) within a CHI are located. + Default value - conf.d + user: + type: string + description: | + Path to the folder where ClickHouse configuration files with users settings are located. + Files are common for all instances within a CHI. + Default value - users.d + user: + type: object + description: "Default parameters for any user which will create" + properties: + default: + type: object + properties: + profile: + type: string + description: "ClickHouse server configuration `<profile>...</profile>` for any <user>" + quota: + type: string + description: "ClickHouse server configuration `<quota>...</quota>` for any <user>" + networksIP: + type: array + description: "ClickHouse server configuration `<networks><ip>...</ip></networks>` for any <user>" + items: + type: string + password: + type: string + description: "ClickHouse server configuration `<password>...</password>` for any <user>" + network: + type: object + description: "Default network parameters for any user which will create" + properties: + hostRegexpTemplate: + type: string + description: "ClickHouse server configuration `<host_regexp>...</host_regexp>` for any <user>" + configurationRestartPolicy: + type: object + description: "Configuration restart policy describes what configuration changes require ClickHouse restart" + properties: + rules: + type: array + description: "Array of set of rules per specified ClickHouse versions" + items: + type: object + properties: + version: + type: string + description: "ClickHouse version expression" + rules: + type: array + description: "Set of configuration rules for specified ClickHouse version" + items: + type: object + description: "setting: value pairs for configuration restart policy" + x-kubernetes-preserve-unknown-fields: true + access: + type: object + description: "parameters which use for connect to clickhouse from clickhouse-operator deployment" + properties: + scheme: + type: string + description: "The scheme to user for connecting to ClickHouse. Possible values: http, https, auto" + username: + type: string + description: "ClickHouse username to be used by operator to connect to ClickHouse instances, deprecated, use chCredentialsSecretName" + password: + type: string + description: "ClickHouse password to be used by operator to connect to ClickHouse instances, deprecated, use chCredentialsSecretName" + rootCA: + type: string + description: "Root certificate authority that clients use when verifying server certificates. Used for https connection to ClickHouse" + secret: + type: object + properties: + namespace: + type: string + description: "Location of k8s Secret with username and password to be used by operator to connect to ClickHouse instances" + name: + type: string + description: "Name of k8s Secret with username and password to be used by operator to connect to ClickHouse instances" + port: + type: integer + minimum: 1 + maximum: 65535 + description: "Port to be used by operator to connect to ClickHouse instances" + timeouts: + type: object + description: "Timeouts used to limit connection and queries from the operator to ClickHouse instances, In seconds" + properties: + connect: + type: integer + minimum: 1 + maximum: 10 + description: "Timout to setup connection from the operator to ClickHouse instances. In seconds." + query: + type: integer + minimum: 1 + maximum: 600 + description: "Timout to perform SQL query from the operator to ClickHouse instances. In seconds." + addons: + type: object + description: "Configuration addons specifies additional settings" + properties: + rules: + type: array + description: "Array of set of rules per specified ClickHouse versions" + items: + type: object + properties: + version: + type: string + description: "ClickHouse version expression" + spec: + type: object + description: "spec" + properties: + configuration: + type: object + description: "allows configure multiple aspects and behavior for `clickhouse-server` instance and also allows describe multiple `clickhouse-server` clusters inside one `chi` resource" + properties: + users: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + profiles: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + quotas: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + settings: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + files: + type: object + description: "see same section from CR spec" + x-kubernetes-preserve-unknown-fields: true + metrics: + type: object + description: "parameters which use for connect to fetch metrics from clickhouse by clickhouse-operator" + properties: + timeouts: + type: object + description: | + Timeouts used to limit connection and queries from the metrics exporter to ClickHouse instances + Specified in seconds. + properties: + collect: + type: integer + minimum: 1 + maximum: 600 + description: | + Timeout used to limit metrics collection request. In seconds. + Upon reaching this timeout metrics collection is aborted and no more metrics are collected in this cycle. + All collected metrics are returned. + tablesRegexp: + type: string + description: | + Regexp to match tables in system database to fetch metrics from. + Multiple tables can be matched using regexp. Matched tables are merged using merge() table function. + Default is "^(metrics|custom_metrics)$". + template: + type: object + description: "Parameters which are used if you want to generate ClickHouseInstallationTemplate custom resources from files which are stored inside clickhouse-operator deployment" + properties: + chi: + type: object + properties: + policy: + type: string + description: | + CHI template updates handling policy + Possible policy values: + - ReadOnStart. Accept CHIT updates on the operators start only. + - ApplyOnNextReconcile. Accept CHIT updates at all time. Apply news CHITs on next regular reconcile of the CHI + enum: + - "" + - "ReadOnStart" + - "ApplyOnNextReconcile" + path: + type: string + description: "Path to folder where ClickHouseInstallationTemplate .yaml manifests are located." + reconcile: + type: object + description: "allow tuning reconciling process" + properties: + runtime: + type: object + description: "runtime parameters for clickhouse-operator process which are used during reconcile cycle" + properties: + reconcileCHIsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "How many goroutines will be used to reconcile CHIs in parallel, 10 by default" + reconcileShardsThreadsNumber: + type: integer + minimum: 1 + maximum: 65535 + description: "How many goroutines will be used to reconcile shards of a cluster in parallel, 1 by default" + reconcileShardsMaxConcurrencyPercent: + type: integer + minimum: 0 + maximum: 100 + description: "The maximum percentage of cluster shards that may be reconciled in parallel, 50 percent by default." + statefulSet: + type: object + description: "Allow change default behavior for reconciling StatefulSet which generated by clickhouse-operator" + properties: + create: + type: object + description: "Behavior during create StatefulSet" + properties: + onFailure: + type: string + description: | + What to do in case created StatefulSet is not in Ready after `statefulSetUpdateTimeout` seconds + Possible options: + 1. abort - do nothing, just break the process and wait for admin. + 2. delete - delete newly created problematic StatefulSet. + 3. ignore (default) - ignore error, pretend nothing happened and move on to the next StatefulSet. + update: + type: object + description: "Behavior during update StatefulSet" + properties: + timeout: + type: integer + description: "How many seconds to wait for created/updated StatefulSet to be Ready" + pollInterval: + type: integer + description: "How many seconds to wait between checks for created/updated StatefulSet status" + onFailure: + type: string + description: | + What to do in case updated StatefulSet is not in Ready after `statefulSetUpdateTimeout` seconds + Possible options: + 1. abort - do nothing, just break the process and wait for admin. + 2. rollback (default) - delete Pod and rollback StatefulSet to previous Generation. Pod would be recreated by StatefulSet based on rollback-ed configuration. + 3. ignore - ignore error, pretend nothing happened and move on to the next StatefulSet. + recreate: + type: object + description: "Behavior during recreate StatefulSet" + properties: + onDataLoss: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to PVC data loss or missing volumes. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate (default) - proceed and recreate StatefulSet. + onUpdateFailure: + type: string + description: | + What to do in case operator needs to recreate StatefulSet due to update failure or StatefulSet not ready. + Possible options: + 1. abort - abort the process, do nothing with the problematic StatefulSet. + 2. recreate (default) - proceed and recreate StatefulSet. + host: + type: object + description: | + Whether the operator during reconcile procedure should wait for a ClickHouse host: + - to be excluded from a ClickHouse cluster + - to complete all running queries + - to be included into a ClickHouse cluster + respectfully before moving forward + properties: + wait: + type: object + properties: + exclude: + type: string + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be excluded from a ClickHouse cluster" + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + queries: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to complete all running queries" + include: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for a ClickHouse host to be included into a ClickHouse cluster" + replicas: + type: object + description: "Whether the operator during reconcile procedure should wait for replicas to catch-up" + properties: + all: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for all replicas to catch-up" + new: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator during reconcile procedure should wait for new replicas to catch-up" + delay: + type: integer + description: "replication max absolute delay to consider replica is not delayed" + probes: + type: object + description: "What probes the operator should wait during host launch procedure" + properties: + startup: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for startup probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to do not wait. + readiness: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during host launch procedure should wait for readiness probe to succeed. + In case probe is unspecified wait is assumed to be completed successfully. + Default option value is to wait. + drop: + type: object + properties: + replicas: + type: object + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted or recreated + properties: + onDelete: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica is deleted + onLostVolume: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop replicas when replica volume is lost + active: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether the operator during reconcile procedure should drop active replicas when replica is deleted or recreated + annotation: + type: object + description: "defines which metadata.annotations items will include or exclude during render StatefulSet, Pod, PVC resources" + properties: + include: + type: array + description: | + When propagating labels from the chi's `metadata.annotations` section to child objects' `metadata.annotations`, + include annotations with names from the following list + items: + type: string + exclude: + type: array + description: | + When propagating labels from the chi's `metadata.annotations` section to child objects' `metadata.annotations`, + exclude annotations with names from the following list + items: + type: string + label: + type: object + description: "defines which metadata.labels will include or exclude during render StatefulSet, Pod, PVC resources" + properties: + include: + type: array + description: | + When propagating labels from the chi's `metadata.labels` section to child objects' `metadata.labels`, + include labels from the following list + items: + type: string + exclude: + type: array + items: + type: string + description: | + When propagating labels from the chi's `metadata.labels` section to child objects' `metadata.labels`, + exclude labels from the following list + appendScope: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Whether to append *Scope* labels to StatefulSet and Pod + - "LabelShardScopeIndex" + - "LabelReplicaScopeIndex" + - "LabelCHIScopeIndex" + - "LabelCHIScopeCycleSize" + - "LabelCHIScopeCycleIndex" + - "LabelCHIScopeCycleOffset" + - "LabelClusterScopeIndex" + - "LabelClusterScopeCycleSize" + - "LabelClusterScopeCycleIndex" + - "LabelClusterScopeCycleOffset" + metrics: + type: object + description: "defines metrics exporter options" + properties: + labels: + type: object + description: "defines metric labels options" + properties: + exclude: + type: array + description: | + When adding labels to a metric exclude labels with names from the following list + items: + type: string + status: + type: object + description: "defines status options" + properties: + fields: + type: object + description: "defines status fields options" + properties: + action: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator should fill status field 'action'" + actions: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator should fill status field 'actions'" + error: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator should fill status field 'error'" + errors: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: "Whether the operator should fill status field 'errors'" + statefulSet: + type: object + description: "define StatefulSet-specific parameters" + properties: + revisionHistoryLimit: + type: integer + description: "revisionHistoryLimit is the maximum number of revisions that will be\nmaintained in the StatefulSet's revision history. \nLook details in `statefulset.spec.revisionHistoryLimit`\n" + pod: + type: object + description: "define pod specific parameters" + properties: + terminationGracePeriod: + type: integer + description: "Optional duration in seconds the pod needs to terminate gracefully. \nLook details in `pod.spec.terminationGracePeriodSeconds`\n" + logger: + type: object + description: "allow setup clickhouse-operator logger behavior" + properties: + logtostderr: + type: string + description: "boolean, allows logs to stderr" + alsologtostderr: + type: string + description: "boolean allows logs to stderr and files both" + v: + type: string + description: "verbosity level of clickhouse-operator log, default - 1 max - 9" + stderrthreshold: + type: string + vmodule: + type: string + description: | + Comma-separated list of filename=N, where filename (can be a pattern) must have no .go ext, and N is a V level. + Ex.: file*=2 sets the 'V' to 2 in all files with names like file*. + log_backtrace_at: + type: string + description: | + It can be set to a file and line number with a logging line. + Ex.: file.go:123 + Each time when this line is being executed, a stack trace will be written to the Info log. +--- +# Template Parameters: +# +# OPERATOR_VERSION=0.26.3 +# +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clickhousekeeperinstallations.clickhouse-keeper.altinity.com + labels: + clickhouse-keeper.altinity.com/chop: 0.26.3 +spec: + group: clickhouse-keeper.altinity.com + scope: Namespaced + names: + kind: ClickHouseKeeperInstallation + singular: clickhousekeeperinstallation + plural: clickhousekeeperinstallations + shortNames: + - chk + versions: + - name: v1 + served: true + storage: true + additionalPrinterColumns: + - name: status + type: string + description: Resource status + jsonPath: .status.status + - name: version + type: string + description: Operator version + priority: 1 # show in wide view + jsonPath: .status.chop-version + - name: clusters + type: integer + description: Clusters count + jsonPath: .status.clusters + - name: shards + type: integer + description: Shards count + priority: 1 # show in wide view + jsonPath: .status.shards + - name: hosts + type: integer + description: Hosts count + jsonPath: .status.hosts + - name: taskID + type: string + description: TaskID + priority: 1 # show in wide view + jsonPath: .status.taskID + - name: hosts-completed + type: integer + description: Completed hosts count + jsonPath: .status.hostsCompleted + - name: hosts-updated + type: integer + description: Updated hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsUpdated + - name: hosts-added + type: integer + description: Added hosts count + priority: 1 # show in wide view + jsonPath: .status.hostsAdded + - name: hosts-deleted + type: integer + description: Hosts deleted count + priority: 1 # show in wide view + jsonPath: .status.hostsDeleted + - name: endpoint + type: string + description: Client access endpoint + priority: 1 # show in wide view + jsonPath: .status.endpoint + - name: age + type: date + description: Age of the resource + # Displayed in all priorities + jsonPath: .metadata.creationTimestamp + - name: suspend + type: string + description: Suspend reconciliation + # Displayed in all priorities + jsonPath: .spec.suspend + subresources: + status: {} + schema: + openAPIV3Schema: + description: "define a set of Kubernetes resources (StatefulSet, PVC, Service, ConfigMap) which describe behavior one or more clusters" + type: object + required: + - spec + properties: + apiVersion: + description: | + APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: | + Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + type: object + description: | + Status contains many fields like a normalized configuration, clickhouse-operator version, current action and all applied action list, current taskID and all applied taskIDs and other + properties: + chop-version: + type: string + description: "Operator version" + chop-commit: + type: string + description: "Operator git commit SHA" + chop-date: + type: string + description: "Operator build date" + chop-ip: + type: string + description: "IP address of the operator's pod which managed this resource" + clusters: + type: integer + minimum: 0 + description: "Clusters count" + shards: + type: integer + minimum: 0 + description: "Shards count" + replicas: + type: integer + minimum: 0 + description: "Replicas count" + hosts: + type: integer + minimum: 0 + description: "Hosts count" + status: + type: string + description: "Status" + taskID: + type: string + description: "Current task id" + taskIDsStarted: + type: array + description: "Started task ids" + nullable: true + items: + type: string + taskIDsCompleted: + type: array + description: "Completed task ids" + nullable: true + items: + type: string + action: + type: string + description: "Action" + actions: + type: array + description: "Actions" + nullable: true + items: + type: string + error: + type: string + description: "Last error" + errors: + type: array + description: "Errors" + nullable: true + items: + type: string + hostsUnchanged: + type: integer + minimum: 0 + description: "Unchanged Hosts count" + hostsUpdated: + type: integer + minimum: 0 + description: "Updated Hosts count" + hostsAdded: + type: integer + minimum: 0 + description: "Added Hosts count" + hostsCompleted: + type: integer + minimum: 0 + description: "Completed Hosts count" + hostsDeleted: + type: integer + minimum: 0 + description: "Deleted Hosts count" + hostsDelete: + type: integer + minimum: 0 + description: "About to delete Hosts count" + pods: + type: array + description: "Pods" + nullable: true + items: + type: string + pod-ips: + type: array + description: "Pod IPs" + nullable: true + items: + type: string + fqdns: + type: array + description: "Pods FQDNs" + nullable: true + items: + type: string + endpoint: + type: string + description: "Endpoint" + endpoints: + type: array + description: "All endpoints" + nullable: true + items: + type: string + generation: + type: integer + minimum: 0 + description: "Generation" + normalized: + type: object + description: "Normalized resource requested" + nullable: true + x-kubernetes-preserve-unknown-fields: true + normalizedCompleted: + type: object + description: "Normalized resource completed" + nullable: true + x-kubernetes-preserve-unknown-fields: true + hostsWithTablesCreated: + type: array + description: "List of hosts with tables created by the operator" + nullable: true + items: + type: string + hostsWithReplicaCaughtUp: + type: array + description: "List of hosts with replica caught up" + nullable: true + items: + type: string + usedTemplates: + type: array + description: "List of templates used to build this CHI" + nullable: true + x-kubernetes-preserve-unknown-fields: true + items: + type: object + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + # x-kubernetes-preserve-unknown-fields: true + description: | + Specification of the desired behavior of one or more ClickHouse clusters + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md + properties: + taskID: + type: string + description: | + Allows to define custom taskID for CHI update and watch status of this update execution. + Displayed in all .status.taskID* fields. + By default (if not filled) every update of CHI manifest will generate random taskID + stop: + type: string + description: | + Allows to stop all ClickHouse Keeper clusters defined in a CHK. + Works as the following: + - When `stop` is `1` operator sets `Replicas: 0` in each StatefulSet. Thie leads to having all `Pods` and `Service` deleted. All PVCs are kept intact. + - When `stop` is `0` operator sets `Replicas: 1` and `Pod`s and `Service`s will created again and all retained PVCs will be attached to `Pod`s. + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + suspend: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Suspend reconciliation of resources managed by a ClickHouse Keeper. + Works as the following: + - When `suspend` is `true` operator stops reconciling all resources. + - When `suspend` is `false` or not set, operator reconciles all resources. + namespaceDomainPattern: + type: string + description: | + Custom domain pattern which will be used for DNS names of `Service` or `Pod`. + Typical use scenario - custom cluster domain in Kubernetes cluster + Example: %s.svc.my.test + reconciling: + type: object + description: "Optional, allows tuning reconciling cycle for ClickhouseInstallation from clickhouse-operator side" + # nullable: true + properties: + policy: + type: string + description: | + DISCUSSED TO BE DEPRECATED + Syntax sugar + Overrides all three 'reconcile.host.wait.{exclude, queries, include}' values from the operator's config + Possible values: + - wait - should wait to exclude host, complete queries and include host back into the cluster + - nowait - should NOT wait to exclude host, complete queries and include host back into the cluster + enum: + - "" + - "wait" + - "nowait" + configMapPropagationTimeout: + type: integer + description: | + Timeout in seconds for `clickhouse-operator` to wait for modified `ConfigMap` to propagate into the `Pod` + More details: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically + minimum: 0 + maximum: 3600 + cleanup: + type: object + description: "Optional, defines behavior for cleanup Kubernetes resources during reconcile cycle" + # nullable: true + properties: + unknownObjects: + type: object + description: | + Describes what clickhouse-operator should do with found Kubernetes resources which should be managed by clickhouse-operator, + but do not have `ownerReference` to any currently managed `ClickHouseInstallation` resource. + Default behavior is `Delete`" + # nullable: true + properties: + statefulSet: + type: string + description: "Behavior policy for unknown StatefulSet, `Delete` by default" + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown PVC, `Delete` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown ConfigMap, `Delete` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for unknown Service, `Delete` by default" + reconcileFailedObjects: + type: object + description: | + Describes what clickhouse-operator should do with Kubernetes resources which are failed during reconcile. + Default behavior is `Retain`" + # nullable: true + properties: + statefulSet: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed StatefulSet, `Retain` by default" + pvc: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed PVC, `Retain` by default" + configMap: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed ConfigMap, `Retain` by default" + service: + type: string + enum: + # List ObjectsCleanupXXX constants from model + - "" + - "Retain" + - "Delete" + description: "Behavior policy for failed Service, `Retain` by default" + defaults: + type: object + description: | + define default behavior for whole ClickHouseInstallation, some behavior can be re-define on cluster, shard and replica level + More info: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#specdefaults + # nullable: true + properties: + replicasUseFQDN: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + define should replicas be specified by FQDN in `<host></host>`. + In case of "no" will use short hostname and clickhouse-server will use kubernetes default suffixes for DNS lookup + "no" by default + distributedDDL: + type: object + description: | + allows change `<yandex><distributed_ddl></distributed_ddl></yandex>` settings + More info: https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings-distributed_ddl + # nullable: true + properties: + profile: + type: string + description: "Settings from this profile will be used to execute DDL queries" + storageManagement: + type: object + description: default storage management options + properties: + provisioner: + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + templates: + type: object + description: "optional, configuration of the templates names which will use for generate Kubernetes resources according to one or more ClickHouse clusters described in current ClickHouseInstallation (chi) resource" + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + configuration: + type: object + description: "allows configure multiple aspects and behavior for `clickhouse-server` instance and also allows describe multiple `clickhouse-server` clusters inside one `chi` resource" + # nullable: true + properties: + settings: + type: object + description: | + allows configure multiple aspects and behavior for `clickhouse-keeper` instance + # nullable: true + x-kubernetes-preserve-unknown-fields: true + files: + type: object + description: | + allows define content of any setting + # nullable: true + x-kubernetes-preserve-unknown-fields: true + clusters: + type: array + description: | + describes clusters layout and allows change settings on cluster-level and replica-level + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "cluster name, used to identify set of servers and wide used during generate names of related Kubernetes resources" + minLength: 1 + # See namePartClusterMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` only in one cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` + override top-level `chi.spec.configuration.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` on current cluster during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected cluster + override top-level `chi.spec.configuration.templates` + pdbManaged: + type: string + enum: + # List StringBoolXXX constants from model + - "" + - "0" + - "1" + - "False" + - "false" + - "True" + - "true" + - "No" + - "no" + - "Yes" + - "yes" + - "Off" + - "off" + - "On" + - "on" + - "Disable" + - "disable" + - "Enable" + - "enable" + - "Disabled" + - "disabled" + - "Enabled" + - "enabled" + description: | + Specifies whether the Pod Disruption Budget (PDB) should be managed. + During the next installation, if PDB management is enabled, the operator will + attempt to retrieve any existing PDB. If none is found, it will create a new one + and initiate a reconciliation loop. If PDB management is disabled, the existing PDB + will remain intact, and the reconciliation loop will not be executed. By default, + PDB management is enabled. + pdbMaxUnavailable: + type: integer + description: | + Pod eviction is allowed if at most "pdbMaxUnavailable" pods are unavailable after the eviction, + i.e. even in absence of the evicted pod. For example, one can prevent all voluntary evictions + by specifying 0. This is a mutually exclusive setting with "minAvailable". + minimum: 0 + maximum: 65535 + layout: + type: object + description: | + describe current cluster layout, how much shards in cluster, how much replica in shard + allows override settings on each shard and replica separatelly + # nullable: true + properties: + replicasCount: + type: integer + description: | + how much replicas in each shards for current cluster will run in Kubernetes, + each replica is a separate `StatefulSet` which contains only one `Pod` with `clickhouse-server` instance, + every shard contains 1 replica by default" + replicas: + type: array + description: "optional, allows override top-level `chi.spec.configuration` and cluster-level `chi.spec.configuration.clusters` configuration for each replica and each shard relates to selected replica, use it only if you fully understand what you do" + # nullable: true + items: + type: object + properties: + name: + type: string + description: "optional, by default replica name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartShardMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and will ignore if shard-level `chi.spec.configuration.clusters.layout.shards` present + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates` + shardsCount: + type: integer + description: "optional, count of shards related to current replica, you can override each shard behavior on low-level `chi.spec.configuration.clusters.layout.replicas.shards`" + minimum: 1 + shards: + type: array + description: "optional, list of shards related to current replica, will ignore if `chi.spec.configuration.clusters.layout.shards` presents" + # nullable: true + items: + # Host + type: object + properties: + name: + type: string + description: "optional, by default shard name is generated, but you can override it and setup custom name" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + zkPort: + type: integer + minimum: 1 + maximum: 65535 + raftPort: + type: integer + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + override top-level `chi.spec.configuration.settings`, cluster-level `chi.spec.configuration.clusters.settings` and replica-level `chi.spec.configuration.clusters.layout.replicas.settings` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` only in one shard related to current replica during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + override top-level `chi.spec.configuration.files` and cluster-level `chi.spec.configuration.clusters.files`, will ignore if `chi.spec.configuration.clusters.layout.shards` presents + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: | + optional, configuration of the templates names which will use for generate Kubernetes resources according to selected replica + override top-level `chi.spec.configuration.templates`, cluster-level `chi.spec.configuration.clusters.templates`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates` + templates: + type: object + description: "allows define templates which will use for render Kubernetes resources like StatefulSet, ConfigMap, Service, PVC, by default, clickhouse-operator have own templates, but you can override it" + # nullable: true + properties: + hostTemplates: + type: array + description: "hostTemplate will use during apply to generate `clickhose-server` config files" + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.hostTemplate`, cluster-level `chi.spec.configuration.clusters.templates.hostTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.hostTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.hostTemplate`" + type: string + portDistribution: + type: array + description: "define how will distribute numeric values of named ports in `Pod.spec.containers.ports` and clickhouse-server configs" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "type of distribution, when `Unspecified` (default value) then all listen ports on clickhouse-server configuration in all Pods will have the same value, when `ClusterScopeIndex` then ports will increment to offset from base value depends on shard and replica index inside cluster with combination of `chi.spec.templates.podTemlates.spec.HostNetwork` it allows setup ClickHouse cluster inside Kubernetes and provide access via external network bypass Kubernetes internal network" + enum: + # List PortDistributionXXX constants + - "" + - "Unspecified" + - "ClusterScopeIndex" + spec: + # Host + type: object + properties: + name: + type: string + description: "by default, hostname will generate, but this allows define custom name for each `clickhouse-server`" + minLength: 1 + # See namePartReplicaMaxLen const + maxLength: 15 + pattern: "^[a-zA-Z0-9-]{0,15}$" + zkPort: + type: integer + minimum: 1 + maximum: 65535 + raftPort: + type: integer + minimum: 1 + maximum: 65535 + settings: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows configure `clickhouse-server` settings inside <yandex>...</yandex> tag in each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/conf.d/` + More details: https://clickhouse.tech/docs/en/operations/settings/settings/ + files: + type: object + # nullable: true + x-kubernetes-preserve-unknown-fields: true + description: | + optional, allows define content of any setting file inside each `Pod` where this template will apply during generate `ConfigMap` which will mount in `/etc/clickhouse-server/config.d/` or `/etc/clickhouse-server/conf.d/` or `/etc/clickhouse-server/users.d/` + templates: + type: object + # nullable: true + properties: + hostTemplate: + type: string + description: "optional, template name from chi.spec.templates.hostTemplates, which will apply to configure every `clickhouse-server` instance during render ConfigMap resources which will mount into `Pod`" + podTemplate: + type: string + description: "optional, template name from chi.spec.templates.podTemplates, allows customization each `Pod` resource during render and reconcile each StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + dataVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + logVolumeClaimTemplate: + type: string + description: "optional, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse log directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + serviceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates. used for customization of the `Service` resource, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + serviceTemplates: + type: array + description: "optional, template names from chi.spec.templates.serviceTemplates. used for customization of the `Service` resources, created by `clickhouse-operator` to cover all clusters in whole `chi` resource" + nullable: true + items: + type: string + clusterServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each clickhouse cluster described in `chi.spec.configuration.clusters`" + shardServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each shard inside clickhouse cluster described in `chi.spec.configuration.clusters`" + replicaServiceTemplate: + type: string + description: "optional, template name from chi.spec.templates.serviceTemplates, allows customization for each `Service` resource which will created by `clickhouse-operator` which cover each replica inside each shard inside each clickhouse cluster described in `chi.spec.configuration.clusters`" + volumeClaimTemplate: + type: string + description: "optional, alias for dataVolumeClaimTemplate, template name from chi.spec.templates.volumeClaimTemplates, allows customization each `PVC` which will mount for clickhouse data directory in each `Pod` during render and reconcile every StatefulSet.spec resource described in `chi.spec.configuration.clusters`" + description: "be careful, this part of CRD allows override template inside template, don't use it if you don't understand what you do" + podTemplates: + type: array + description: | + podTemplate will use during render `Pod` inside `StatefulSet.spec` and allows define rendered `Pod.spec`, pod scheduling distribution and pod zone + More information: https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatespodtemplates + # nullable: true + items: + type: object + #required: + # - name + properties: + name: + type: string + description: "template name, could use to link inside top-level `chi.spec.defaults.templates.podTemplate`, cluster-level `chi.spec.configuration.clusters.templates.podTemplate`, shard-level `chi.spec.configuration.clusters.layout.shards.temlates.podTemplate`, replica-level `chi.spec.configuration.clusters.layout.replicas.templates.podTemplate`" + generateName: + type: string + description: "allows define format for generated `Pod` name, look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates for details about available template variables" + zone: + type: object + description: "allows define custom zone name and will separate ClickHouse `Pods` between nodes, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + #required: + # - values + properties: + key: + type: string + description: "optional, if defined, allows select kubernetes nodes by label with `name` equal `key`" + values: + type: array + description: "optional, if defined, allows select kubernetes nodes by label with `value` in `values`" + # nullable: true + items: + type: string + distribution: + type: string + description: "DEPRECATED, shortcut for `chi.spec.templates.podTemplates.spec.affinity.podAntiAffinity`" + enum: + - "" + - "Unspecified" + - "OnePerHost" + podDistribution: + type: array + description: "define ClickHouse Pod distribution policy between Kubernetes Nodes inside Shard, Replica, Namespace, CHI, another ClickHouse cluster" + # nullable: true + items: + type: object + #required: + # - type + properties: + type: + type: string + description: "you can define multiple affinity policy types" + enum: + # List PodDistributionXXX constants + - "" + - "Unspecified" + - "ClickHouseAntiAffinity" + - "ShardAntiAffinity" + - "ReplicaAntiAffinity" + - "AnotherNamespaceAntiAffinity" + - "AnotherClickHouseInstallationAntiAffinity" + - "AnotherClusterAntiAffinity" + - "MaxNumberPerNode" + - "NamespaceAffinity" + - "ClickHouseInstallationAffinity" + - "ClusterAffinity" + - "ShardAffinity" + - "ReplicaAffinity" + - "PreviousTailAffinity" + - "CircularReplication" + scope: + type: string + description: "scope for apply each podDistribution" + enum: + # list PodDistributionScopeXXX constants + - "" + - "Unspecified" + - "Shard" + - "Replica" + - "Cluster" + - "ClickHouseInstallation" + - "Namespace" + number: + type: integer + description: "define, how much ClickHouse Pods could be inside selected scope with selected distribution type" + minimum: 0 + maximum: 65535 + topologyKey: + type: string + description: | + use for inter-pod affinity look to `pod.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution.podAffinityTerm.topologyKey`, + more info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity" + metadata: + type: object + description: | + allows pass standard object's metadata from template to Pod + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify PodSpec + type: object + description: "allows define whole Pod.spec inside StaefulSet.spec, look to https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates for details" + # nullable: true + x-kubernetes-preserve-unknown-fields: true + volumeClaimTemplates: + type: array + description: | + allows define template for rendering `PVC` kubernetes resource, which would use inside `Pod` for mount clickhouse `data`, clickhouse `logs` or something else + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + top-level `chi.spec.defaults.templates.dataVolumeClaimTemplate` or `chi.spec.defaults.templates.logVolumeClaimTemplate`, + cluster-level `chi.spec.configuration.clusters.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.templates.logVolumeClaimTemplate`, + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.shards.temlates.logVolumeClaimTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.dataVolumeClaimTemplate` or `chi.spec.configuration.clusters.layout.replicas.templates.logVolumeClaimTemplate` + provisioner: + type: string + description: "defines `PVC` provisioner - be it StatefulSet or the Operator" + enum: + - "" + - "StatefulSet" + - "Operator" + reclaimPolicy: + type: string + description: | + defines behavior of `PVC` deletion. + `Delete` by default, if `Retain` specified then `PVC` will be kept when deleting StatefulSet + enum: + - "" + - "Retain" + - "Delete" + metadata: + type: object + description: | + allows to pass standard object's metadata from template to PVC + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + type: object + description: | + allows define all aspects of `PVC` resource + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims + # nullable: true + x-kubernetes-preserve-unknown-fields: true + serviceTemplates: + type: array + description: | + allows define template for rendering `Service` which would get endpoint from Pods which scoped chi-wide, cluster-wide, shard-wide, replica-wide level + # nullable: true + items: + type: object + #required: + # - name + # - spec + properties: + name: + type: string + description: | + template name, could use to link inside + chi-level `chi.spec.defaults.templates.serviceTemplate` + cluster-level `chi.spec.configuration.clusters.templates.clusterServiceTemplate` + shard-level `chi.spec.configuration.clusters.layout.shards.temlates.shardServiceTemplate` + replica-level `chi.spec.configuration.clusters.layout.replicas.templates.replicaServiceTemplate` or `chi.spec.configuration.clusters.layout.shards.replicas.replicaServiceTemplate` + generateName: + type: string + description: | + allows define format for generated `Service` name, + look to https://github.com/Altinity/clickhouse-operator/blob/master/docs/custom_resource_explained.md#spectemplatesservicetemplates + for details about available template variables" + metadata: + # TODO specify ObjectMeta + type: object + description: | + allows pass standard object's metadata from template to Service + Could be use for define specificly for Cloud Provider metadata which impact to behavior of service + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + # nullable: true + x-kubernetes-preserve-unknown-fields: true + spec: + # TODO specify ServiceSpec + type: object + description: | + describe behavior of generated Service + More info: https://kubernetes.io/docs/concepts/services-networking/service/ + # nullable: true + x-kubernetes-preserve-unknown-fields: true diff --git a/pkg/vendored/altinity-clickhouse/deployment/affinity.go b/pkg/vendored/altinity-clickhouse/deployment/affinity.go new file mode 100644 index 00000000..f5039c41 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/deployment/affinity.go @@ -0,0 +1,62 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package deployment + +// Possible pod distributions +const ( + PodDistributionUnspecified = "Unspecified" + // AntiAffinity section + PodDistributionClickHouseAntiAffinity = "ClickHouseAntiAffinity" + PodDistributionShardAntiAffinity = "ShardAntiAffinity" + PodDistributionReplicaAntiAffinity = "ReplicaAntiAffinity" + PodDistributionAnotherNamespaceAntiAffinity = "AnotherNamespaceAntiAffinity" + PodDistributionAnotherClickHouseInstallationAntiAffinity = "AnotherClickHouseInstallationAntiAffinity" + PodDistributionAnotherClusterAntiAffinity = "AnotherClusterAntiAffinity" + // Affinity section + PodDistributionNamespaceAffinity = "NamespaceAffinity" + PodDistributionClickHouseInstallationAffinity = "ClickHouseInstallationAffinity" + PodDistributionClusterAffinity = "ClusterAffinity" + PodDistributionShardAffinity = "ShardAffinity" + PodDistributionReplicaAffinity = "ReplicaAffinity" + PodDistributionPreviousTailAffinity = "PreviousTailAffinity" + // Misc section + PodDistributionMaxNumberPerNode = "MaxNumberPerNode" + PodDistributionMaxNumberPerNodeEqualsReplicasCount = 2000000000 + // Shortcuts section + PodDistributionCircularReplication = "CircularReplication" + + PodDistributionScopeUnspecified = "Unspecified" + // Pods from different ClickHouseInstallation.Cluster.Shard can co-exist on one node + PodDistributionScopeShard = "Shard" + // Pods from different ClickHouseInstallation.Cluster.Replica can co-exist on one node + PodDistributionScopeReplica = "Replica" + // Pods from different ClickHouseInstallation.Cluster can co-exist on one node + PodDistributionScopeCluster = "Cluster" + // Pods from different ClickHouseInstallations can co-exist on one node + PodDistributionScopeClickHouseInstallation = "ClickHouseInstallation" + // Pods from different Namespaces can co-exist on one node + PodDistributionScopeNamespace = "Namespace" + // No Pods can co-exist on one node + PodDistributionScopeGlobal = "Global" + + // Deprecated value + PodDistributionOnePerHost = "OnePerHost" +) + +// Possible port distributions +const ( + PortDistributionUnspecified = "Unspecified" + PortDistributionClusterScopeIndex = "ClusterScopeIndex" +) diff --git a/pkg/vendored/altinity-clickhouse/deployment/env_vars.go b/pkg/vendored/altinity-clickhouse/deployment/env_vars.go new file mode 100644 index 00000000..b85878d8 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/deployment/env_vars.go @@ -0,0 +1,56 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package deployment + +const ( + // OPERATOR_POD_NODE_NAME name from spec.nodeName Ex.: ip-172-20-52-62.ec2.internal + OPERATOR_POD_NODE_NAME = "OPERATOR_POD_NODE_NAME" + // OPERATOR_POD_NAME name from metadata.name Ex.: clickhouse-operator-6f87589dbb-ftcsf + OPERATOR_POD_NAME = "OPERATOR_POD_NAME" + // OPERATOR_POD_NAMESPACE name from metadata.namespace Ex.: kube-system + OPERATOR_POD_NAMESPACE = "OPERATOR_POD_NAMESPACE" + // OPERATOR_POD_IP ip from status.podIP Ex.: 100.96.3.2 + OPERATOR_POD_IP = "OPERATOR_POD_IP" + // OPERATOR_POD_SERVICE_ACCOUNT service account + // from + // spec.serviceAccount + // spec.serviceAccountName + // Ex.: clickhouse-operator + OPERATOR_POD_SERVICE_ACCOUNT = "OPERATOR_POD_SERVICE_ACCOUNT" + + // OPERATOR_CONTAINER_CPU_REQUEST from .containers.resources.requests.cpu + OPERATOR_CONTAINER_CPU_REQUEST = "OPERATOR_CONTAINER_CPU_REQUEST" + // OPERATOR_CONTAINER_CPU_LIMIT from .containers.resources.limits.cpu + OPERATOR_CONTAINER_CPU_LIMIT = "OPERATOR_CONTAINER_CPU_LIMIT" + // OPERATOR_CONTAINER_MEM_REQUEST from .containers.resources.requests.memory + OPERATOR_CONTAINER_MEM_REQUEST = "OPERATOR_CONTAINER_MEM_REQUEST" + // OPERATOR_CONTAINER_MEM_LIMIT from .containers.resources.limits.memory + OPERATOR_CONTAINER_MEM_LIMIT = "OPERATOR_CONTAINER_MEM_LIMIT" + + // OPERATOR_K8S_CLIENT_QPS_LIMIT specifies an override for the default k8s client QPS rate limit. + OPERATOR_K8S_CLIENT_QPS_LIMIT = "OPERATOR_K8S_CLIENT_QPS_LIMIT" + // OPERATOR_K8S_CLIENT_BURST_LIMIT specifies an override for the default k8s client QPS burst limit. + OPERATOR_K8S_CLIENT_BURST_LIMIT = "OPERATOR_K8S_CLIENT_BURST_LIMIT" + + // WATCH_NAMESPACE and WATCH_NAMESPACES specifies what namespaces to watch + WATCH_NAMESPACE = "WATCH_NAMESPACE" + // WATCH_NAMESPACES and WATCH_NAMESPACE specifies what namespaces to watch + WATCH_NAMESPACES = "WATCH_NAMESPACES" + // WATCH_NAMESPACES_EXCLUDE specifies namespaces that should be excluded from reconciliation + WATCH_NAMESPACES_EXCLUDE = "WATCH_NAMESPACES_EXCLUDE" + + // CHOP_CONFIG path to clickhouse operator configuration file + CHOP_CONFIG = "CHOP_CONFIG" +) diff --git a/pkg/vendored/altinity-clickhouse/metrics/watched_resource.go b/pkg/vendored/altinity-clickhouse/metrics/watched_resource.go new file mode 100644 index 00000000..a5cb45b8 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/metrics/watched_resource.go @@ -0,0 +1,156 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package metrics + +import ( + "encoding/json" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" +) + +// WatchedCR specifies watched ClickHouseInstallation +type WatchedCR struct { + Namespace string `json:"namespace"` + Name string `json:"name"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` + Clusters []*WatchedCluster `json:"clusters"` +} + +// WatchedCluster specifies watched cluster +type WatchedCluster struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Hosts []*WatchedHost `json:"hosts,omitempty" yaml:"hosts,omitempty"` +} + +// WatchedHost specifies watched host +type WatchedHost struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Hostname string `json:"hostname,omitempty" yaml:"hostname,omitempty"` + TCPPort int32 `json:"tcpPort,omitempty" yaml:"tcpPort,omitempty"` + TLSPort int32 `json:"tlsPort,omitempty" yaml:"tlsPort,omitempty"` + HTTPPort int32 `json:"httpPort,omitempty" yaml:"httpPort,omitempty"` + HTTPSPort int32 `json:"httpsPort,omitempty" yaml:"httpsPort,omitempty"` +} + +// NewWatchedCR creates new watched CR +func NewWatchedCR(src v1.ICustomResource) *WatchedCR { + cr := &WatchedCR{} + cr.readFrom(src) + return cr +} + +func (cr *WatchedCR) readFrom(src v1.ICustomResource) { + if cr == nil { + return + } + cr.Namespace = src.GetNamespace() + cr.Name = src.GetName() + cr.Labels = src.GetLabels() + cr.Annotations = src.GetAnnotations() + + src.WalkClusters(func(cl v1.ICluster) error { + cluster := &WatchedCluster{} + cluster.readFrom(cl) + cr.Clusters = append(cr.Clusters, cluster) + return nil + }) +} + +func (cr *WatchedCR) IsValid() bool { + return !cr.empty() +} + +func (cr *WatchedCR) empty() bool { + return (len(cr.Namespace) == 0) && (len(cr.Name) == 0) && (len(cr.Clusters) == 0) +} + +func (cr *WatchedCR) IndexKey() string { + return cr.Namespace + ":" + cr.Name +} + +func (cr *WatchedCR) WalkHosts(f func(*WatchedCR, *WatchedCluster, *WatchedHost)) { + if cr == nil { + return + } + for _, cluster := range cr.Clusters { + for _, host := range cluster.Hosts { + f(cr, cluster, host) + } + } +} + +func (cr *WatchedCR) GetName() string { + if cr == nil { + return "" + } + return cr.Name +} + +func (cr *WatchedCR) GetNamespace() string { + if cr == nil { + return "" + } + return cr.Namespace +} + +func (cr *WatchedCR) GetLabels() map[string]string { + if cr == nil { + return nil + } + return cr.Labels +} + +func (cr *WatchedCR) GetAnnotations() map[string]string { + if cr == nil { + return nil + } + return cr.Annotations +} + +// String is a stringifier +func (cr *WatchedCR) String() string { + if cr == nil { + return "nil" + } + bytes, _ := json.Marshal(cr) + return string(bytes) +} + +func (cluster *WatchedCluster) readFrom(c v1.ICluster) { + if cluster == nil { + return + } + cluster.Name = c.GetName() + + c.WalkHosts(func(h *v1.Host) error { + host := &WatchedHost{} + host.readFrom(h) + cluster.Hosts = append(cluster.Hosts, host) + return nil + }) +} + +func (host *WatchedHost) readFrom(h *v1.Host) { + if host == nil { + return + } + host.Name = h.Name + host.Hostname = h.Runtime.Address.FQDN + host.TCPPort = h.TCPPort.Value() + host.TLSPort = h.TLSPort.Value() + host.HTTPPort = h.HTTPPort.Value() + host.HTTPSPort = h.HTTPSPort.Value() +} diff --git a/pkg/vendored/altinity-clickhouse/swversion/min_max.go b/pkg/vendored/altinity-clickhouse/swversion/min_max.go new file mode 100644 index 00000000..f815290d --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/swversion/min_max.go @@ -0,0 +1,23 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package swversion + +func MinVersion() *SoftWareVersion { + return NewSoftWareVersion("0.0.1") +} + +func MaxVersion() *SoftWareVersion { + return NewSoftWareVersion("99.99.99") +} diff --git a/pkg/vendored/altinity-clickhouse/swversion/software_version.go b/pkg/vendored/altinity-clickhouse/swversion/software_version.go new file mode 100644 index 00000000..529618ad --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/swversion/software_version.go @@ -0,0 +1,190 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package swversion + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/Masterminds/semver/v3" +) + +// SoftWareVersion specifies software version and software semver +type SoftWareVersion struct { + // original specifies original software version, such as 21.9.6.24-alpha + original string + // normalized specifies semver-compatible - version truncated to 3 numbers, such as 21.9.6 for 21.9.6.24-alpha + normalized string + // description specifies description if needed + description string + // semver specifies semver version + semver *semver.Version +} + +func (in *SoftWareVersion) DeepCopy() *SoftWareVersion { + if in == nil { + return nil + } + out := new(SoftWareVersion) + in.DeepCopyInto(out) + return out +} + +func (in *SoftWareVersion) DeepCopyInto(out *SoftWareVersion) { + *out = *in + if in.semver != nil { + in, out := &in.semver, &out.semver + *out = new(semver.Version) + *out = *in + } + return +} + +// NewSoftWareVersion creates new software version +// version - specifies original software version, such as: 21 or 21.1 or 21.9.6.24-alpha +func NewSoftWareVersion(version string) *SoftWareVersion { + if strings.TrimSpace(version) == "" { + return nil + } + + // Fetch comma-separated parts of the software version + parts := strings.Split(version, ".") + + // Need to have at least something to as a major version + if len(parts) < 1 { + return nil + } + + // Need to have at least 3 parts in software version specification + for len(parts) < 3 { + parts = append(parts, "0") + } + + // Take first 3 parts and ensure they are digits + parts = parts[0:3] + for _, part := range parts { + if _, err := strconv.Atoi(part); err != nil { + return nil + } + } + + // Normalized version of the original + normalized := strings.Join(parts, ".") + + // Build version + _semver, err := semver.NewVersion(normalized) + if err != nil { + return nil + } + + return &SoftWareVersion{ + original: version, + normalized: normalized, + semver: _semver, + } +} + +func NewSoftWareVersionFromTag(tag string) *SoftWareVersion { + if strings.ToLower(strings.TrimSpace(tag)) == "latest" { + return MaxVersion() + } + + r := regexp.MustCompile(`\d+(\.\d+)+`) + return NewSoftWareVersion(r.FindString(tag)) +} + +// Matches checks whether software version matches specified constraint or not +func (v *SoftWareVersion) Matches(constraint string) bool { + if v == nil { + return false + } + + c, err := semver.NewConstraint(constraint) + if err != nil { + return false + } + + // Validate a version against a constraint. + matches, _ := c.Validate(v.semver) + + return matches +} + +// Cmp compares two versions +func (v *SoftWareVersion) Cmp(to *SoftWareVersion) int { + if v.IsUnknown() || to.IsUnknown() { + // Need both versions to compare + return 0 + } + return v.semver.Compare(to.semver) +} + +// IsUnknown checks whether software version is unknown or not +func (v *SoftWareVersion) IsUnknown() bool { + if v == nil { + // Version is unknown + return true + } + if len(v.normalized) == 0 { + // Version is unknown + return true + } + if v.semver == nil { + // Version is unknown + return true + } + + // Version known + return false +} + +// IsKnown checks whether software version is unknown or not +func (v *SoftWareVersion) IsKnown() bool { + return !v.IsUnknown() +} + +func (v *SoftWareVersion) SetDescription(format string, args ...interface{}) *SoftWareVersion { + if v == nil { + return nil + } + v.description = fmt.Sprintf(format, args...) + return v +} + +// String makes a string +func (v *SoftWareVersion) String() string { + if v == nil { + return "" + } + return v.normalized +} + +// Render makes a string +func (v *SoftWareVersion) Render() string { + if v == nil { + return "" + } + return v.normalized + "[" + v.original + "/" + v.description + "]" +} + +// GetOriginal is a getter +func (v *SoftWareVersion) GetOriginal() string { + if v == nil { + return "" + } + return v.original +} diff --git a/pkg/vendored/altinity-clickhouse/util/array.go b/pkg/vendored/altinity-clickhouse/util/array.go new file mode 100644 index 00000000..931f9092 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/array.go @@ -0,0 +1,204 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "bytes" + "fmt" + "regexp" + "sort" +) + +// InArray checks whether the needle is in the haystack +func InArray(needle string, haystack []string) bool { + for _, item := range haystack { + if item == needle { + return true + } + } + return false +} + +// MatchArrayOfRegexps checks whether the needle can be matched by haystack +func MatchArrayOfRegexps(needle string, haystack []string) bool { + for _, pattern := range haystack { + matched, _ := regexp.MatchString(pattern, needle) + if pattern == needle || matched { + return true + } + } + return false +} + +// MergeStringArrays appends into dst items from src that are not present in src. src items are being deduplicated +func MergeStringArrays(dst []string, src []string) []string { + for _, item := range src { + if !InArray(item, dst) { + dst = append(dst, item) + } + } + return dst +} + +// IntersectStringArrays intersects arrays `a` and `b` +func IntersectStringArrays(a []string, b []string) (res []string) { + for _, item := range a { + if InArray(item, b) { + res = append(res, item) + } + } + return res +} + +// RemoveFromArray removes the needle from the haystack +func RemoveFromArray(needle string, haystack []string) []string { + result := []string{} + + for _, item := range haystack { + if item == needle { + continue + } + result = append(result, item) + } + + return result +} + +// Unzip makes two 1-value columns (slices) out of one 2-value column (slice) +func Unzip(slice [][]string) ([]string, []string) { + col1 := make([]string, 0, len(slice)) + col2 := make([]string, 0, len(slice)) + for i := 0; i < len(slice); i++ { + col1 = append(col1, slice[i][0]) + if len(slice[i]) > 1 { + col2 = append(col2, slice[i][1]) + } + } + return col1, col2 +} + +// CastToSliceOfStrings makes slice of strings from map +func CastToSliceOfStrings(m map[string]interface{}) []string { + res := make([]string, 0, 0) + + // Sort keys + var keys []string + for key := range m { + keys = append(keys, key) + } + sort.Strings(keys) + + // Walk over sorted keys + for _, key := range keys { + res = append(res, key) + + switch m[key].(type) { + case string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + value := fmt.Sprint(m[key]) + res = append(res, value) + case []string, []int, []int8, []int16, []int32, []int64, []uint, []uint8, []uint16, []uint32, []uint64, []float32, []float64, []interface{}: + for _, v := range m[key].([]interface{}) { + value := fmt.Sprint(v) + res = append(res, value) + } + } + } + + return res +} + +// Slice2String returns named slice as a string +func Slice2String(name string, slice []string) string { + b := &bytes.Buffer{} + Fprintf(b, "%s (%d):\n", name, len(slice)) + for i := range slice { + Fprintf(b, " - %s\n", slice[i]) + } + + return b.String() +} + +func Prefix(slice []string, prefix string) (res []string) { + for _, entry := range slice { + res = append(res, prefix+entry) + } + return res +} + +func Unique(slice []string) (unique []string) { + found := make(map[string]bool) + + for _, entry := range slice { + found[entry] = true + } + + for entry := range found { + unique = append(unique, entry) + } + + return unique +} + +func NonEmpty(slice []string) (nonEmpty []string) { + for _, str := range slice { + if str != "" { + nonEmpty = append(nonEmpty, str) + } + } + return nonEmpty +} + +func ConcatSlices[T any](slices [][]T) []T { + var totalLen int + + for _, s := range slices { + totalLen += len(s) + } + + result := make([]T, totalLen) + + var i int + + for _, s := range slices { + i += copy(result[i:], s) + } + + return result +} + +// SliceDistinct returns distinct slice +func SliceDistinct[T comparable](slice []T) (distinct []T) { + m := make(map[T]struct{}, len(slice)) + for _, i := range slice { + if _, ok := m[i]; !ok { + m[i] = struct{}{} + distinct = append(distinct, i) + } + } + + return distinct +} + +func SlicesIntersect[T comparable](a, b []T) (intersection []T) { + bDistinct := SliceDistinct(b) + for _, i := range SliceDistinct(a) { + for _, j := range bDistinct { + if i == j { + intersection = append(intersection, i) + } + } + } + return intersection +} diff --git a/pkg/vendored/altinity-clickhouse/util/context.go b/pkg/vendored/altinity-clickhouse/util/context.go new file mode 100644 index 00000000..a00f7ceb --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/context.go @@ -0,0 +1,85 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "context" + "time" +) + +// IsContextDone is a non-blocking one-word syntactic sugar to check whether context is done. +// Convenient to be used as +// +// if IsContextDone(ctx) { +// ... do something ... +// } +func IsContextDone(ctx context.Context) bool { + if ctx == nil { + // In case there is no context, it can not be done + return false + } + + select { + case <-ctx.Done(): + return true + default: + return false + } +} + +// WaitContextDoneOrTimeout waits either for ctx to be done or specified timeout +// returns true in case ctx is Done or false in case timeout reached +func WaitContextDoneOrTimeout(ctx context.Context, timeout time.Duration) bool { + select { + case <-ctx.Done(): + return true + case <-time.After(timeout): + return false + } +} + +// WaitContextDoneUntil waits either for ctx to be done or specified moment in time reached +// returns true in case ctx is Done or false in case specified time reached +func WaitContextDoneUntil(ctx context.Context, t time.Time) bool { + select { + case <-ctx.Done(): + return true + case <-time.After(time.Until(t)): + return false + } +} + +// ContextError is a one-word syntactic sugar to check what error is reported by the context. +func ContextError(ctx context.Context) error { + if ctx == nil { + // In case there is no context, there is no context error + return nil + } + + return ctx.Err() +} + +// ErrIsNotCanceled checks whether specified error is either not an error is is not a context.Canceled +func ErrIsNotCanceled(err error) bool { + if err == nil { + return false + } + + if err == context.Canceled { + return false + } + + return true +} diff --git a/pkg/vendored/altinity-clickhouse/util/dir.go b/pkg/vendored/altinity-clickhouse/util/dir.go new file mode 100644 index 00000000..a3877d53 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/dir.go @@ -0,0 +1,58 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import "path/filepath" + +// RelativeToBasePath returns absolute path relative to base +func RelativeToBasePath(basePath, relativePath string) string { + if basePath == "" { + // Relative base is not set, no base to be relative to, consider relative path to be absolute + return relativePath + } + + // Relative base is set - try to be relative to it + if absPath, err := filepath.Abs(basePath + "/" + relativePath); err == nil { + // Absolute path is fine + return absPath + } + + // Unable to build correct absolute path, this is an error + return "" +} + +// PreparePath - prepares path absolute/relative with default relative value +func PreparePath(path *string, basePath, defaultRelativePath string) { + switch { + case *path == "": + // Path is not specified at all. + // Build path as 'default' path relative to the specified base + *path = RelativeToBasePath(basePath, defaultRelativePath) + case filepath.IsAbs(*path): + // Path is specified as an absolute path. + // Absolute paths are considered to be prepared already. + // Nothing to do here. + default: + // Path is specified as relative path. + // Build result path as provided relative path relative to the base path + *path = RelativeToBasePath(basePath, *path) + } + + // Specified path should exist + // In case of incorrect/unavailable path - make it empty + if (*path != "") && !IsDirOk(*path) { + *path = "" + } +} diff --git a/pkg/vendored/altinity-clickhouse/util/dump.go b/pkg/vendored/altinity-clickhouse/util/dump.go new file mode 100644 index 00000000..ce84fa0e --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/dump.go @@ -0,0 +1,25 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import dumper "github.com/sanity-io/litter" + +// Dump dumps object into string with repeatable order +func Dump(obj interface{}) string { + d := dumper.Options{ + Separator: " ", + } + return d.Sdump(obj) +} diff --git a/pkg/vendored/altinity-clickhouse/util/duration.go b/pkg/vendored/altinity-clickhouse/util/duration.go new file mode 100644 index 00000000..80743e14 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/duration.go @@ -0,0 +1,30 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import "time" + +// ReasonableDuration finds first duration > 0 or returns 0 in case no durations provided +func ReasonableDuration(durations ...time.Duration) time.Duration { + if len(durations) == 0 { + return 0 + } + for _, duration := range durations { + if duration > 0 { + return duration + } + } + return 0 +} diff --git a/pkg/vendored/altinity-clickhouse/util/fingerprint.go b/pkg/vendored/altinity-clickhouse/util/fingerprint.go new file mode 100644 index 00000000..3cf6fadb --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/fingerprint.go @@ -0,0 +1,20 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +// Fingerprint creates object's fingerprint +func Fingerprint(obj interface{}) string { + return HashIntoString(serializeRepeatable(obj)) +} diff --git a/pkg/vendored/altinity-clickhouse/util/fqdn.go b/pkg/vendored/altinity-clickhouse/util/fqdn.go new file mode 100644 index 00000000..0fbeabee --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/fqdn.go @@ -0,0 +1,32 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import "strings" + +// NormalizeFQDN strips the trailing dot from an FQDN for storage/display. +// DNS connections use the full form with dot; status fields use the clean form. +func NormalizeFQDN(fqdn string) string { + return strings.TrimSuffix(fqdn, ".") +} + +// NormalizeFQDNs returns a copy of fqdns with trailing dots stripped from each entry. +func NormalizeFQDNs(fqdns []string) []string { + result := make([]string, len(fqdns)) + for i, f := range fqdns { + result[i] = NormalizeFQDN(f) + } + return result +} diff --git a/pkg/vendored/altinity-clickhouse/util/fs.go b/pkg/vendored/altinity-clickhouse/util/fs.go new file mode 100644 index 00000000..a7971428 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/fs.go @@ -0,0 +1,75 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "os" + "path/filepath" + "strings" +) + +// IsDirOk returns whether the given path exists and is a dir +func IsDirOk(path string) bool { + if stat, err := os.Stat(path); (err == nil) && stat.IsDir() { + // File object Stat-ed without errors - it exists and it is a dir + return true + } + + // Some kind of error has happened + return false +} + +// ReadFilesIntoMap reads config files from specified path into "file name->file content" map +// path - folder where to look for files +// isOurFile - accepts path to file return bool whether this file should be read +func ReadFilesIntoMap(path string, isOurFile func(string) bool) map[string]string { + // Look in real path only + if path == "" { + return nil + } + + // Result is a filename to content map + var files map[string]string + + // Loop over all files in folder + if matches, err := filepath.Glob(path + "/*"); err == nil { + for i := range matches { + // `file` comes with `path`-prefixed. + // So in case `path` is an absolute path, `file` will be absolute path to file + file := matches[i] + if isOurFile(file) { + // Pick our files only + if content, err := os.ReadFile(filepath.Clean(file)); (err == nil) && (len(content) > 0) { + // File content read successfully and file has some content + if files == nil { + files = make(map[string]string) + } + // Use short filename (file.ext) as a key for the content + files[filepath.Base(file)] = string(content) + } + } + } + } + + if len(files) > 0 { + return files + } + return nil +} + +// ExtToLower fetches and lower-cases file extension. With dot, as '.xml' +func ExtToLower(file string) string { + return strings.ToLower(filepath.Ext(file)) +} diff --git a/pkg/vendored/altinity-clickhouse/util/hash.go b/pkg/vendored/altinity-clickhouse/util/hash.go new file mode 100644 index 00000000..af1de066 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/hash.go @@ -0,0 +1,78 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "bytes" + // #nosec + // G505 (CWE-327): Blocklisted import crypto/sha1: weak cryptographic primitive + // It is good enough for string ID + "crypto/sha1" + "encoding/gob" + "encoding/hex" + "fmt" + "hash/fnv" + + dumper "github.com/sanity-io/litter" + // "github.com/davecgh/go-spew/spew" +) + +func serializeUnrepeatable(obj interface{}) []byte { + b := bytes.Buffer{} + encoder := gob.NewEncoder(&b) + err := encoder.Encode(obj) + if err != nil { + fmt.Println(`failed gob Encode`, err) + } + + return b.Bytes() +} + +func serializeRepeatable(obj interface{}) []byte { + //s := spew.NewDefaultConfig() + //s.SortKeys = true + d := dumper.Options{ + Separator: " ", + } + return []byte(d.Sdump(obj)) +} + +// HashIntoString hashes bytes and returns string version of the hash +func HashIntoString(b []byte) string { + if len(b) == 0 { + return "" + } + // #nosec + // G401 (CWE-326): Use of weak cryptographic primitive + // It is good enough for string ID + hasher := sha1.New() + hasher.Write(b) + return hex.EncodeToString(hasher.Sum(nil)) +} + +// HashIntoInt hashes bytes and returns int version of the hash +func HashIntoInt(b []byte) int { + if len(b) == 0 { + return 0 + } + h := fnv.New32a() + _, _ = h.Write(b) + return int(h.Sum32()) +} + +// HashIntoIntTopped hashes bytes and return int version of the ash topped with top +func HashIntoIntTopped(b []byte, top int) int { + return HashIntoInt(b) % top +} diff --git a/pkg/vendored/altinity-clickhouse/util/int.go b/pkg/vendored/altinity-clickhouse/util/int.go new file mode 100644 index 00000000..6a0db33a --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/int.go @@ -0,0 +1,31 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +func IncTopped(start, inc, top int) int { + res := start + inc + if res > top { + res = top + } + return res +} + +func DecBottomed(start, dec, bottom int) int { + res := start - dec + if res < bottom { + res = bottom + } + return res +} diff --git a/pkg/vendored/altinity-clickhouse/util/k8s.go b/pkg/vendored/altinity-clickhouse/util/k8s.go new file mode 100644 index 00000000..dc513933 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/k8s.go @@ -0,0 +1,83 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + core "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" +) + +type iNamespaceName interface { + GetNamespace() string + GetName() string +} + +// NamespaceName returns namespace and anme from the meta +func NamespaceName(meta iNamespaceName) (string, string) { + return meta.GetNamespace(), meta.GetName() +} + +// NamespaceNameString returns namespace and name as one string +func NamespaceNameString(meta iNamespaceName) string { + return meta.GetNamespace() + "/" + meta.GetName() +} + +// NamespacedName returns NamespacedName from obj +func NamespacedName(obj iNamespaceName) types.NamespacedName { + return types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + } +} + +// AnnotationsToBeSkipped kubectl service annotation that we'd like to skip +var AnnotationsToBeSkipped = []string{ + "kubectl.kubernetes.io/last-applied-configuration", +} + +// IsAnnotationToBeSkipped checks whether an annotation should be skipped +func IsAnnotationToBeSkipped(annotation string) bool { + for _, a := range AnnotationsToBeSkipped { + if a == annotation { + return true + } + } + return false +} + +// ListSkippedAnnotations provides list of annotations that should be skipped +func ListSkippedAnnotations() []string { + return AnnotationsToBeSkipped +} + +// MergeEnvVars appends to `to` elements from `from` which are not found in `to` +func MergeEnvVars(to []core.EnvVar, from ...core.EnvVar) []core.EnvVar { + for _, candidate := range from { + if !HasEnvVar(to, candidate) { + to = append(to, candidate) + } + } + return to +} + +// HasEnvVar checks whether a haystack has a needle +func HasEnvVar(haystack []core.EnvVar, needle core.EnvVar) bool { + for _, envVar := range haystack { + if needle.Name == envVar.Name { + return true + } + } + return false +} diff --git a/pkg/vendored/altinity-clickhouse/util/map.go b/pkg/vendored/altinity-clickhouse/util/map.go new file mode 100644 index 00000000..57879e08 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/map.go @@ -0,0 +1,350 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "bytes" + "reflect" + "slices" + + "golang.org/x/exp/constraints" +) + +// IncludeNonEmpty inserts (and overwrites) data into map object using specified key, if not empty value provided +func IncludeNonEmpty[TKey comparable, TValue any](dst map[TKey]TValue, key TKey, value TValue) { + // Do not include empty value + v := reflect.ValueOf(value) + if v.IsZero() { + return + } + + // Include (and overwrite) value by specified key + dst[key] = value +} + +// CopyMap creates a copy of the given map by copying over key by key. +// It doesn't perform a deep-copy. +func CopyMap[TKey comparable, TValue any](src map[TKey]TValue) map[TKey]TValue { + result := make(map[TKey]TValue, len(src)) + for key, value := range src { + result[key] = value + } + return result +} + +// CopyMapFilter copies maps with keys filtering. +// Keys specified in 'include' are included, +// keys specified in 'exclude' are excluded. +// However, 'include' keys are applied only in case 'include' list is not empty. +func CopyMapFilter[TKey comparable, TValue any](src map[TKey]TValue, includeKeys, excludeKeys []TKey) map[TKey]TValue { + return CopyMapExclude(CopyMapInclude(src, includeKeys...), excludeKeys...) +} + +// CopyMapInclude creates a copy of the given map but will include the given set of keys only. +// However, keys are applied only in case list is not empty. +// In case of an empty list, no filtering is performed and all keys are copied. +func CopyMapInclude[TKey comparable, TValue any](src map[TKey]TValue, includeKeys ...TKey) map[TKey]TValue { + if len(includeKeys) == 0 { + // No include list specified, just copy the whole map + return CopyMap(src) + } + + // Include list specified, copy listed keys only + result := make(map[TKey]TValue, len(includeKeys)) + for _, key := range includeKeys { + if value, ok := src[key]; ok { + result[key] = value + } + } + return result +} + +// CopyMapExclude creates a copy of the given map but will exclude the given set of keys. +func CopyMapExclude[TKey comparable, TValue any](src map[TKey]TValue, excludeKeys ...TKey) map[TKey]TValue { + result := CopyMap(src) + + for _, key := range excludeKeys { + delete(result, key) + } + + return result +} + +// MergeStringMapsOverwrite inserts (and overwrites) data into dst map object from src +func MergeStringMapsOverwrite[TKey comparable, TValue any](dst, src map[TKey]TValue, keys ...TKey) map[TKey]TValue { + if len(src) == 0 { + // Nothing to merge from + return dst + } + + var created bool + if dst == nil { + dst = make(map[TKey]TValue) + created = true + } + + // Place key->value pair from src into dst + + if len(keys) == 0 { + // No explicitly specified keys to merge, just merge the whole src + for key := range src { + dst[key] = src[key] + } + } else { + // We have explicitly specified list of keys to merge from src + for _, key := range keys { + if value, ok := src[key]; ok { + dst[key] = value + } + } + } + + if created && (len(dst) == 0) { + return nil + } + + return dst +} + +// MergeStringMapsPreserve inserts (and preserved existing) data into dst map object from src +func MergeStringMapsPreserve[TKey comparable, TValue any](dst, src map[TKey]TValue, keys ...TKey) map[TKey]TValue { + if len(src) == 0 { + // Nothing to merge from + return dst + } + + var created bool + if dst == nil { + dst = make(map[TKey]TValue) + created = true + } + + // Place key->value pair from src into dst + + if len(keys) == 0 { + // No explicitly specified keys to merge, just merge the whole src + for key := range src { + if _, ok := dst[key]; !ok { + dst[key] = src[key] + } + } + } else { + // We have explicitly specified list of keys to merge from src + for _, key := range keys { + if value, ok := src[key]; ok { + if _, ok := dst[key]; !ok { + dst[key] = value + } + } + } + } + + if created && (len(dst) == 0) { + return nil + } + + return dst +} + +// SubtractStringMaps subtracts "delta" from "base" by keys +func SubtractStringMaps[TKey comparable, TValue any](base, delta map[TKey]TValue) map[TKey]TValue { + if len(delta) == 0 { + // Nothing to delete + return base + } + if len(base) == 0 { + // Nowhere to delete from + return base + } + + // Extract keys from delta and delete them from base + for key := range delta { + if _, ok := base[key]; ok { + delete(base, key) + } + } + + return base +} + +// MapDeleteKeys deletes multiple keys from the map +func MapDeleteKeys[TKey comparable, TValue any](m map[TKey]TValue, keys ...TKey) map[TKey]TValue { + if len(m) == 0 { + // Nowhere to delete from + return m + } + if len(keys) == 0 { + // Nothing to delete + return m + } + + // Extract delete keys from base + for _, key := range keys { + if _, ok := m[key]; ok { + delete(m, key) + } + } + + return m +} + +// MapHasKeys checks whether map has all keys from specified list +func MapHasKeys[TKey comparable, TValue any](m map[TKey]TValue, keys ...TKey) bool { + if len(m) == 0 { + return false + } + if len(keys) == 0 { + return false + } + + for _, needle := range keys { + // Have we found this needle + if _, found := m[needle]; !found { + // No, not found + return false + } + } + + return true +} + +func cmp[T constraints.Ordered](a, b T) int { + if a < b { + return -1 + } + if a > b { + return 1 + } + return 0 +} + +// Map2String returns named map as a string +func Map2String[TKey constraints.Ordered, TValue any](name string, m map[TKey]TValue) string { + // Write map entries according to sorted keys + // So we need to + // 1. Extract and sort all keys + // 2. Walk over keys and write map entries + + // 1. Sort keys + keys := MapGetSortedKeys(m) + + // Walk over sorted keys + b := &bytes.Buffer{} + Fprintf(b, "%s (%d):\n", name, len(m)) + for _, key := range keys { + // VENDORED CHANGE: Use %v instead of %s to support any comparable type + Fprintf(b, " - [%v]=%v\n", key, m[key]) + } + + return b.String() +} + +func MapGetKeys[TKey comparable, TValue any](m map[TKey]TValue) (keys []TKey) { + if m == nil { + return nil + } + for key := range m { + keys = append(keys, key) + } + return keys +} + +func MapGetSortedKeys[TKey constraints.Ordered, TValue any](m map[TKey]TValue) (keys []TKey) { + if m == nil { + return nil + } + for key := range m { + keys = append(keys, key) + } + slices.SortStableFunc(keys, cmp) + //sort.Strings(keys) + return keys +} + +func MapGetSortedKeysAndValues[TKey constraints.Ordered, TValue any](m map[TKey]TValue) (keys []TKey, values []TValue) { + if m == nil { + return nil, nil + } + keys = MapGetSortedKeys(m) + for _, key := range keys { + if value, ok := m[key]; ok { + values = append(values, value) + } + } + return keys, values +} + +func MapMigrate[TKey constraints.Ordered, TValue any](cur, new, old map[TKey]TValue) map[TKey]TValue { + removed := MapGetSortedKeys(SubtractStringMaps(CopyMap(old), new)) + return MapDeleteKeys(MergeStringMapsPreserve(new, cur), removed...) +} + +func MapsAreTheSame[TKey comparable, TValue comparable](m1, m2 map[TKey]TValue) bool { + if len(m1) != len(m2) { + // Different set means not equal + return false + } + + for k1, v1 := range m1 { + v2, found := m2[k1] + if !found { + // B has no key from A + return false + } + // A and B has the same key + // Values has to be the same to be considered equal + if v1 != v2 { + return false + } + } + + return true +} + +func MapsIntersectKeys[TKey comparable, TValue any](m1, m2 map[TKey]TValue) (keysIntersection []TKey) { + keys1 := MapGetKeys(m1) + keys2 := MapGetKeys(m2) + return SlicesIntersect(keys1, keys2) +} + +func MapsHaveKeysIntersection[TKey comparable, TValue any](m1, m2 map[TKey]TValue) bool { + return len(MapsIntersectKeys(m1, m2)) > 0 +} + +func MapsHaveSameKeyValuePairs[TKey comparable, TValue comparable](m1, m2 map[TKey]TValue, keys ...TKey) (same bool) { + if len(keys) == 0 { + return false + } + + same = true + for _, key := range keys { + v1, ok1 := m1[key] + v2, ok2 := m2[key] + if ok1 && ok2 && (v1 == v2) { + // The same + continue + } + same = false + } + + return same +} + +func MapsHaveKeyValuePairsIntersection[TKey comparable, TValue comparable](m1, m2 map[TKey]TValue) bool { + keys1 := MapGetKeys(m1) + keys2 := MapGetKeys(m2) + keys := SlicesIntersect(keys1, keys2) + return MapsHaveSameKeyValuePairs(m1, m2, keys...) +} diff --git a/pkg/vendored/altinity-clickhouse/util/messagediff.go b/pkg/vendored/altinity-clickhouse/util/messagediff.go new file mode 100644 index 00000000..5efbd270 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/messagediff.go @@ -0,0 +1,98 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "fmt" + + "gopkg.in/d4l3k/messagediff.v1" +) + +// MessageDiffString stringifies message diff +func MessageDiffString(diff *messagediff.Diff, equal bool) string { + if equal { + return "" + } + + str := "" + + if len(diff.Added) > 0 { + // Something added + str += MessageDiffItemString("added items", "none", "", diff.Added) + } + + if len(diff.Removed) > 0 { + // Something removed + str += MessageDiffItemString("removed items", "none", "", diff.Removed) + } + + if len(diff.Modified) > 0 { + // Something modified + str += MessageDiffItemString("modified spec items", "none", "", diff.Modified) + } + + return str +} + +// MessageDiffItemString stringifies one map[*messagediff.Path]interface{} item +func MessageDiffItemString(bannerForDiff, bannerForNoDiff, defaultPath string, items map[*messagediff.Path]interface{}) (str string) { + if len(items) == 0 { + return bannerForNoDiff + } + + // Have modified items + str += fmt.Sprintf("Diff start -------------------------\n") + str += fmt.Sprintf("%s num: %d\n", bannerForDiff, len(items)) + + i := 0 + for pathPtr := range items { + path := "" + for _, pathNode := range *pathPtr { + // Format + // .Template + // .Spec + // .Containers + // [0] + // .Ports + // [1] + // .Protocol + // as + // .Template.Spec.Containers[0].Ports[1].Protocol + path += fmt.Sprintf("%v", pathNode) + } + if path == "" { + path = defaultPath + } + + valueShort := fmt.Sprintf("%+v", items[pathPtr]) + valueFull := fmt.Sprintf("%s", Dump(items[pathPtr])) + value := "" + if len(valueFull) < 300 { + value = valueFull + } else { + value = valueShort + } + + //str += fmt.Sprintf("diff item path [%d]:'%s'\n", i, path) + //str += fmt.Sprintf("diff item value[%d]:'%s'\n", i, value) + + str += fmt.Sprintf("diff item [%d]:'%s' = '%s'\n", i, path, value) + + i++ + } + str += fmt.Sprintf("Diff end -------------------------\n") + + return str +} diff --git a/pkg/vendored/altinity-clickhouse/util/print.go b/pkg/vendored/altinity-clickhouse/util/print.go new file mode 100644 index 00000000..99fc3464 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/print.go @@ -0,0 +1,37 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "fmt" + "io" + "strconv" +) + +// Iline writes indented line with \n into provided io.Writer +func Iline(w io.Writer, indent int, format string, a ...interface{}) { + if indent > 0 { + // Prepare indentation template %16s + template := "%" + strconv.Itoa(indent) + "s" + Fprintf(w, template, " ") + } + Fprintf(w, format, a...) + Fprintf(w, "\n") +} + +// Fprintf suppresses warning for unused returns of fmt.Fprintf() +func Fprintf(w io.Writer, format string, a ...interface{}) { + _, _ = fmt.Fprintf(w, format, a...) +} diff --git a/pkg/vendored/altinity-clickhouse/util/prometheus.go b/pkg/vendored/altinity-clickhouse/util/prometheus.go new file mode 100644 index 00000000..bd130b23 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/prometheus.go @@ -0,0 +1,76 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "regexp" +) + +// Labels may contain ASCII letters, numbers, as well as underscores. +// They must match the regex [a-zA-Z_][a-zA-Z0-9_]* +// For more details check: +// https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels + +const prometheusLabelFormat string = "[a-zA-Z_][a-zA-Z0-9_]*" +const prometheusLabelNotAllowedCharsFormat string = "[^a-zA-Z0-9_]" + +var prometheusLabelRegexp = regexp.MustCompile("^" + prometheusLabelFormat + "$") +var prometheusLabelNotAllowedCharsRegexp = regexp.MustCompile(prometheusLabelNotAllowedCharsFormat) + +// Metric names may contain ASCII letters, digits, underscores, and colons. +// It must match the regex [a-zA-Z_:][a-zA-Z0-9_:]* +// For more details check: +// https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels + +const prometheusMetricFormat string = "[a-zA-Z_:][a-zA-Z0-9_:]*" +const prometheusMetricNotAllowedCharsFormat string = "[^a-zA-Z0-9_:]" + +var prometheusMetricRegexp = regexp.MustCompile("^" + prometheusMetricFormat + "$") +var prometheusMetricNotAllowedCharsRegexp = regexp.MustCompile(prometheusMetricNotAllowedCharsFormat) + +func BuildPrometheusLabel(label string) string { + // Replace not allowed chars + return prometheusLabelNotAllowedCharsRegexp.ReplaceAllString(label, "_") +} + +func BuildPrometheusLabels(labels ...string) []string { + var res []string + for _, label := range labels { + res = append(res, BuildPrometheusLabel(label)) + } + return res +} + +// IsValidPrometheusLabel tests for a string that conforms to the definition of a label in Prometheus +func IsValidPrometheusLabel(value string) bool { + return prometheusLabelRegexp.MatchString(value) +} + +func IsValidPrometheusLabelValue(value string) bool { + // Label values may contain any Unicode characters + // For more details check: + // https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels + return true +} + +func BuildPrometheusMetricName(name string) string { + // Replace not allowed chars + return prometheusMetricNotAllowedCharsRegexp.ReplaceAllString(name, "_") +} + +// IsValidPrometheusMetricName tests for a string that conforms to the definition of a metric in Prometheus +func IsValidPrometheusMetricName(value string) bool { + return prometheusMetricRegexp.MatchString(value) +} diff --git a/pkg/vendored/altinity-clickhouse/util/replacer.go b/pkg/vendored/altinity-clickhouse/util/replacer.go new file mode 100644 index 00000000..5c6a84c8 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/replacer.go @@ -0,0 +1,221 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "encoding/json" + "strings" +) + +type Replacer struct { + macroToExpansionMap map[string]string + stringReplacer *strings.Replacer + mapReplacer *MapReplacer + sliceReplacer *SliceReplacer +} + +// NewReplacer +func NewReplacer(macroToExpansionMaps ...map[string]string) *Replacer { + r := &Replacer{ + macroToExpansionMap: make(map[string]string), + } + + // Fill unified expansion map + for _, macroToExpansionMap := range macroToExpansionMaps { + for macro, expansion := range macroToExpansionMap { + r.macroToExpansionMap[macro] = expansion + } + } + + // Fill [replaced, replacement] pairs in slice + var replacements []string + for macro, expansion := range r.macroToExpansionMap { + replacements = append(replacements, macro, expansion) + } + + // Fill replacers + r.stringReplacer = strings.NewReplacer(replacements...) + r.mapReplacer = NewMapReplacer(r.stringReplacer) + r.sliceReplacer = NewSliceReplacer(r.stringReplacer) + + return r +} + +func NewReplacerFrom(from ...*Replacer) *Replacer { + // Combine expansion maps from all replacers + var macroToExpansionMap = make(map[string]string) + for _, replacer := range from { + for macro, expansion := range replacer.macroToExpansionMap { + macroToExpansionMap[macro] = expansion + } + } + + // Build new replacer from combined expansion map + return NewReplacer(macroToExpansionMap) +} + +func (e *Replacer) String() string { + if e == nil { + return "" + } + s, _ := json.Marshal(e.macroToExpansionMap) + return string(s) +} + +// Line expands line with macros(es) +func (e *Replacer) Line(line string) string { + if e == nil { + // No replacement + return line + } + return e.stringReplacer.Replace(line) +} + +// LineEx expands line with macros(es) +func (e *Replacer) LineEx(line string) (string, bool) { + res := e.Line(line) + return res, res != line +} + +// Map expands map with macros(es) +func (e *Replacer) Map(_map map[string]string) map[string]string { + if e == nil { + // No replacement + return _map + } + return e.mapReplacer.Replace(_map) +} + +// MapEx expands map with macros(es) +func (e *Replacer) MapEx(_map map[string]string) (map[string]string, bool) { + return e.mapReplacer.ReplaceEx(_map) +} + +// Slice expands slice with macros(es) +func (e *Replacer) Slice(s []string) []string { + if e == nil { + // No replacement + return s + } + return e.sliceReplacer.Replace(s) +} + +// SliceEx expands slice with macros(es) +func (e *Replacer) SliceEx(s []string) ([]string, bool) { + return e.sliceReplacer.ReplaceEx(s) +} + +// MapReplacer replaces a list of strings with replacements on a map. +type MapReplacer struct { + *strings.Replacer +} + +// NewMapReplacer creates new MapReplacer +func NewMapReplacer(r *strings.Replacer) *MapReplacer { + return &MapReplacer{ + r, + } +} + +// Replace returns a copy of m with all replacements performed. +func (r *MapReplacer) Replace(m map[string]string) map[string]string { + if r == nil { + // No replacement + return m + } + if len(m) == 0 { + // Nothing to replace + return m + } + result := make(map[string]string, len(m)) + for key := range m { + result[r.Replacer.Replace(key)] = r.Replacer.Replace(m[key]) + } + return result +} + +// Replace returns a copy of m with all replacements performed. +func (r *MapReplacer) ReplaceEx(m map[string]string) (map[string]string, bool) { + if r == nil { + // No replacement + return m, false + } + if len(m) == 0 { + // Nothing to replace + return m, false + } + result := make(map[string]string, len(m)) + modified := false + for key, value := range m { + resultKey := r.Replacer.Replace(key) + resultValue := r.Replacer.Replace(value) + result[resultKey] = resultValue + modifiedKey := key != resultKey + modifiedValue := value != resultValue + modified = modified || modifiedKey || modifiedValue + } + return result, modified +} + +// SliceReplacer replaces a list of strings with replacements on a slice. +type SliceReplacer struct { + *strings.Replacer +} + +// NewSliceReplacer creates new SliceReplacer +func NewSliceReplacer(r *strings.Replacer) *SliceReplacer { + return &SliceReplacer{ + r, + } +} + +// Replace returns a copy of m with all replacements performed. +func (r *SliceReplacer) Replace(m []string) []string { + if r == nil { + // No replacement + return m + } + if len(m) == 0 { + // Nothing to replace + return m + } + result := make([]string, len(m)) + for i, value := range m { + resultValue := r.Replacer.Replace(value) + result[i] = resultValue + } + return result +} + +// Replace returns a copy of m with all replacements performed. +func (r *SliceReplacer) ReplaceEx(m []string) ([]string, bool) { + if r == nil { + // No replacement + return m, false + } + if len(m) == 0 { + // Nothing to replace + return m, false + } + result := make([]string, len(m)) + modified := false + for i, value := range m { + resultValue := r.Replacer.Replace(value) + result[i] = resultValue + modifiedValue := value != resultValue + modified = modified || modifiedValue + } + return result, modified +} diff --git a/pkg/vendored/altinity-clickhouse/util/retry/retry.go b/pkg/vendored/altinity-clickhouse/util/retry/retry.go new file mode 100644 index 00000000..bd64a859 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/retry/retry.go @@ -0,0 +1,65 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package retry + +import ( + "context" + "time" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" +) + +// Announcer is a minimal logging interface matching the methods used by Retry. +type Announcer interface { + Info(format string, args ...interface{}) + Warning(format string, args ...interface{}) +} + +// Retry retries specified function +func Retry(ctx context.Context, tries int, desc string, a Announcer, f func() error) error { + var err error + for try := 1; try <= tries; try++ { + if util.IsContextDone(ctx) { + a.Info("ctx is done") + return nil + } + // Do useful things + err = f() + + if err == nil { + // All ok, no need to retry more + if try > 1 { + // Done, but after some retries, this is not 'clean' + a.Info("DONE attempt %d of %d: %s", try, tries, desc) + } + return nil + } + + if try < tries { + // Try failed, need to sleep and retry + seconds := try * 5 + a.Info("FAILED attempt %d of %d, sleep %d sec and retry: %s", try, tries, seconds, desc) + util.WaitContextDoneOrTimeout(ctx, time.Duration(seconds)*time.Second) + } else if tries == 1 { + // On single try do not put so much emotion. It just failed and user is not intended to retry + a.Warning("FAILED single try. No retries will be made for %s", desc) + } else { + // On last try no need to wait more + a.Warning("FAILED AND ABORT. All %d attempts: %s", tries, desc) + } + } + + return err +} diff --git a/pkg/vendored/altinity-clickhouse/util/rfc1035.go b/pkg/vendored/altinity-clickhouse/util/rfc1035.go new file mode 100644 index 00000000..c35d4aef --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/rfc1035.go @@ -0,0 +1,65 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "regexp" + "strings" +) + +const dns1035LabelMaxLength int = 63 +const dns1035LabelFormat string = "[a-z]([-a-z0-9]*[a-z0-9])?" +const dns1035LabelStartFormat string = "[a-z]" +const dns1035LabelNotAllowedCharsFormat string = "[^-a-z0-9]" + +var dns1035LabelRegexp = regexp.MustCompile("^" + dns1035LabelFormat + "$") +var dns1035LabelStartRegexp = regexp.MustCompile("^" + dns1035LabelStartFormat) +var dns1035LabelNotAllowedCharsRegexp = regexp.MustCompile(dns1035LabelNotAllowedCharsFormat) + +func BuildRFC1035Label(str string) (string, bool) { + // Must be lowercase + str = strings.ToLower(str) + // First char must comply to start regexp + for len(str) > 0 { + if dns1035LabelStartRegexp.MatchString(str) { + break + } else { + str = str[1:] + } + } + // Remove not allowed chars + str = dns1035LabelNotAllowedCharsRegexp.ReplaceAllString(str, "") + // Must have limited length + if len(str) > dns1035LabelMaxLength { + str = str[0:dns1035LabelMaxLength] + } + + if IsValidDNS1035Label(str) { + return str, true + } + + return "", false +} + +// IsValidDNS1035Label tests for a string that conforms to the definition of a label in DNS (RFC 1035). +func IsValidDNS1035Label(value string) bool { + if len(value) > dns1035LabelMaxLength { + return false + } + if !dns1035LabelRegexp.MatchString(value) { + return false + } + return true +} diff --git a/pkg/vendored/altinity-clickhouse/util/runtime/runtime.go b/pkg/vendored/altinity-clickhouse/util/runtime/runtime.go new file mode 100644 index 00000000..0871a0d6 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/runtime/runtime.go @@ -0,0 +1,54 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "path" + "reflect" + "runtime" + "strings" +) + +// Caller returns triplet: file name, line number, function name of a caller +func Caller(skip string) (string, int, string) { + pc := make([]uintptr, 7) + n := runtime.Callers(2, pc) + frames := runtime.CallersFrames(pc[:n]) + for { + frame, more := frames.Next() + // frame.File = /tmp/sandbox469341579/prog.go + // frame.Line = 28 + // frame.Function = main.Announcer.Info + + // file = prog.go + file := path.Base(frame.File) + // function = Info + function := path.Base(strings.Replace(frame.Function, ".", "/", -1)) + + if file != skip { + return file, frame.Line, function + } + + if !more { + break + } + } + return "", 0, "" +} + +// FunctionName returns name of the calling function +func FunctionName(fn interface{}) string { + return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() +} diff --git a/pkg/vendored/altinity-clickhouse/util/shell.go b/pkg/vendored/altinity-clickhouse/util/shell.go new file mode 100644 index 00000000..c25ab488 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/shell.go @@ -0,0 +1,87 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "crypto/md5" + "encoding/hex" + "regexp" + "strings" +) + +const shellEnvVarNameBaseMaxLength int = 63 +const shellEnvVarNameFullMaxLength int = 127 + +var shellEnvVarNameRegexp = regexp.MustCompile("^[A-Z]([_A-Z0-9]*[A-Z0-9])?$") +var shellEnvVarNameStartRegexp = regexp.MustCompile("^[A-Z]") +var shellEnvVarNameNotAllowedCharsRegexp = regexp.MustCompile("[^_A-Z0-9]") +var shellEnvVarNameReplaceCharsRegexp = regexp.MustCompile("[/]") + +func BuildShellEnvVarName(str string) (name string, ok bool) { + // Do not touch original value + name = str + + // Must be uppercase + name = strings.ToUpper(name) + + // First char must comply to start regexp + // Cut the first char until it is reasonable + for len(name) > 0 { + if shellEnvVarNameStartRegexp.MatchString(name) { + break + } else { + name = name[1:] + } + } + + // Replace replaceable chars + name = shellEnvVarNameReplaceCharsRegexp.ReplaceAllString(name, "_") + // Remove not allowed chars + name = shellEnvVarNameNotAllowedCharsRegexp.ReplaceAllString(name, "") + + // Must have limited length + suffix := "" + if len(name) > shellEnvVarNameBaseMaxLength { + // Cut the name + name = name[0:shellEnvVarNameBaseMaxLength] + // Prepare fixed length suffix out of original string + hash := md5.Sum([]byte(str)) + suffix = "_" + strings.ToUpper(hex.EncodeToString(hash[:])) + } + + // Ensure no trailing underscores + name = strings.TrimRight(name, "_") + + // Append suffix to keep name uniqueness + name += suffix + + // It still has to be a valid env ma,e after all + if IsShellEnvVarName(name) { + return name, true + } + + return "", false +} + +// IsShellEnvVarName tests for a string that conforms to the definition of a shell ENV VAR name +func IsShellEnvVarName(value string) bool { + if len(value) > shellEnvVarNameFullMaxLength { + return false + } + if !shellEnvVarNameRegexp.MatchString(value) { + return false + } + return true +} diff --git a/pkg/vendored/altinity-clickhouse/util/slice.go b/pkg/vendored/altinity-clickhouse/util/slice.go new file mode 100644 index 00000000..6b897f0c --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/slice.go @@ -0,0 +1,24 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +type Slice[T any] []T + +func (s Slice[T]) First() T { + if len(s) < 1 { + panic("need at least one") + } + return s[0] +} diff --git a/pkg/vendored/altinity-clickhouse/util/string.go b/pkg/vendored/altinity-clickhouse/util/string.go new file mode 100644 index 00000000..3a3bb66c --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/util/string.go @@ -0,0 +1,93 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + // #nosec + // G505 (CWE-327): Blocklisted import crypto/sha1: weak cryptographic primitive + // It is good enough for string ID + "crypto/sha1" + "encoding/hex" + "math/rand" + "time" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// randStringBytes specifies bytes that could be used by RandString generator +const randStringBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +// RandString generates random string of specified length +func RandString(length int) string { + b := make([]byte, length) + for i := range b { + b[i] = randStringBytes[rand.Intn(len(randStringBytes))] + } + return string(b) +} + +// RandStringRange specifies random string with length in specified range +func RandStringRange(minLength, maxLength int) string { + return RandString(rand.Intn(maxLength-minLength+1) + minLength) +} + +// CreateStringID creates HEX hash ID out of a string. +// In case maxHashLen == 0 the whole hash is returned +func CreateStringID(str string, maxHashLen int) string { + // #nosec + // G401 (CWE-326): Use of weak cryptographic primitive + // It is good enough for string ID + sha := sha1.New() + sha.Write([]byte(str)) + hash := hex.EncodeToString(sha.Sum(nil)) + + if maxHashLen == 0 { + // Explicitly requested to return everything + return hash + } + + if maxHashLen >= len(hash) { + // Requested hash len is greater than we have + // Return whole hash - everything what we have + return hash + } + + // Requested hash len is smaller that the hash + // Return last part of the hash + return hash[len(hash)-maxHashLen:] +} + +// StringHead returns beginning of the string of requested length +func StringHead(str string, maxHeadLen int) string { + if len(str) <= maxHeadLen { + // String is shorter than head requested - return everything + return str + } + + // Return beginning of the string + return str[:maxHeadLen] +} + +// StringSliceContains implements contain method for string slice +func StringSliceContains(haystack []string, needle string) bool { + for _, a := range haystack { + if a == needle { + return true + } + } + return false +} diff --git a/pkg/vendored/altinity-clickhouse/version/version.go b/pkg/vendored/altinity-clickhouse/version/version.go new file mode 100644 index 00000000..6c5b321f --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/version/version.go @@ -0,0 +1,24 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package version + +// Version represents the software version +var Version = "dev-version" + +// GitSHA represents the Git commit hash in short format +var GitSHA string + +// BuiltAt represents date + time when binary was built +var BuiltAt string diff --git a/pkg/vendored/altinity-clickhouse/xml/xml.go b/pkg/vendored/altinity-clickhouse/xml/xml.go new file mode 100644 index 00000000..5ce22e86 --- /dev/null +++ b/pkg/vendored/altinity-clickhouse/xml/xml.go @@ -0,0 +1,258 @@ +// Copyright 2019 Altinity Ltd and/or its affiliates. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package xml + +import ( + "fmt" + "io" + "regexp" + "sort" + "strings" + + "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/util" +) + +type xmlNode struct { + children []*xmlNode + tag string + value setting +} + +const ( + eol = "\n" + noEol = "" +) + +type setting interface { + fmt.Stringer + IsEmpty() bool + IsScalar() bool + IsVector() bool + Attributes() string + VectorOfStrings() []string + IsEmbed() bool +} + +type settings interface { + Len() int + WalkNames(func(name string)) + GetA(string) any +} + +// GenerateFromSettings creates XML representation from the provided settings +func GenerateFromSettings(w io.Writer, settings settings, prefix string) { + if settings.Len() == 0 { + return + } + + // `paths` is a sorted set of normalized paths (maps keys) from settings + paths := make([]string, 0, settings.Len()) + + // `data` is a copy of settings with: + // 1. paths (map keys) are normalized in terms of trimmed '/' + // 2. all map keys listed in 'excludes' are excluded + data := make(map[string]string) + // Skip excluded paths + settings.WalkNames(func(name string) { + // 'name' may be non-normalized, and may have starting or trailing '/' + // 'path' is normalized path without starting and trailing '/', ex.: 'test/quotas' + path := normalizePath(prefix, name) + if path == "" { + return + } + paths = append(paths, path) + data[path] = name + }) + sort.Strings(paths) + + // `xmlTreeRoot` - root of the XML tree data structure + xmlTreeRoot := new(xmlNode) + + // Read all tags and values into the tree structure + for _, path := range paths { + // Split path (test/quotas) into tags which would be 'test' and 'quota' + tags := strings.Split(path, "/") + if len(tags) == 0 { + // Empty path? Should not be, but double check + continue + } + name := data[path] + xmlTreeRoot.addBranch(tags, settings.GetA(name).(setting)) + } + + // build XML into writer + xmlTreeRoot.buildXML(w, 0, 4) +} + +// normalizePath makes 'prefix/a/b/c' out of 'prefix' + '/a//b///c////' +// Important - leading '/' is removed! +func normalizePath(prefix, path string) string { + // Normalize '//' to '/' + re := regexp.MustCompile("//+") + path = re.ReplaceAllString(path, "/") + // Cut all leading and trailing '/' + path = strings.Trim(path, "/") + if len(prefix) > 0 { + return prefix + "/" + path + } + + return path +} + +// addBranch ensures branch exists and assign value to the last tagged node +func (n *xmlNode) addBranch(tags []string, setting setting) { + node := n + for _, tag := range tags { + node = node.addChild(tag) + } + node.value = setting +} + +// addChild add new or return existing child with matching tag +func (n *xmlNode) addChild(tag string) *xmlNode { + if n.children == nil { + n.children = make([]*xmlNode, 0, 0) + } + + // Check for such tag exists + for i := range n.children { + if n.children[i].tag == tag { + // Already have such a tag + return n.children[i] + } + } + + // No this is new tag - add as a child + node := &xmlNode{ + tag: tag, + } + n.children = append(n.children, node) + + return node +} + +func (n *xmlNode) NoValue() bool { + return (n.value == nil) || n.value.IsEmpty() +} + +// buildXML generates XML from xmlNode type linked list +func (n *xmlNode) buildXML(w io.Writer, indent, tabSize uint8) { + switch { + case n.NoValue(): + // No value node, may have nested tags + n.writeTagNoValue(w, "", indent, tabSize) + return + + case n.value.IsScalar(): + // ScalarString node + n.writeTagWithValue(w, n.value.String(), n.value.Attributes(), indent, n.value.IsEmbed()) + return + + case n.value.IsVector(): + // VectorOfStrings node + for _, value := range n.value.VectorOfStrings() { + n.writeTagWithValue(w, value, n.value.Attributes(), indent, n.value.IsEmbed()) + } + } +} + +// writeTagNoValue prints tag which has no value, But it may have nested tags +// <a> +// +// <b>...</b> +// +// </a> +func (n *xmlNode) writeTagNoValue(w io.Writer, attributes string, indent, tabSize uint8) { + n.writeTagOpen(w, indent, attributes, eol) + for i := range n.children { + n.children[i].buildXML(w, indent+tabSize, tabSize) + } + n.writeTagClose(w, indent, eol) +} + +// writeTagWithValue prints tag with value. But it must have no children, +// and children are not printed +// <tag>value</tag> +// OR +// <tag> +// embedded value NB - printed w/o indent +// </tag> +func (n *xmlNode) writeTagWithValue(w io.Writer, value string, attributes string, indent uint8, embedded bool) { + // TODO fix this properly + // Used in tests + if value == "_removed_" || value == "_remove_" { + attributes = " remove=\"1\"" + value = "" + } + + if embedded { + // <tag> + // embedded value NB - printed w/o indent + // </tag> + n.writeTagOpen(w, indent, attributes, eol) + n.writeValue(w, value) + n.writeTagClose(w, indent, eol) + } else { + // <tag>value</tag> + n.writeTagOpen(w, indent, attributes, noEol) + n.writeValue(w, value) + n.writeTagClose(w, 0, eol) + } +} + +// writeTagOpen prints open XML tag into io.Writer +func (n *xmlNode) writeTagOpen(w io.Writer, indent uint8, attributes string, eol string) { + n.writeTag(w, indent, attributes, true, eol) +} + +// writeTagClose prints close XML tag into io.Writer +func (n *xmlNode) writeTagClose(w io.Writer, indent uint8, eol string) { + n.writeTag(w, indent, "", false, eol) +} + +// writeTag prints XML tag into io.Writer +func (n *xmlNode) writeTag(w io.Writer, indent uint8, attributes string, openTag bool, eol string) { + if n.tag == "" { + return + } + + // We have to separate indent and no-indent cases, because event target pattern is like + // "%0s</%s> - meaning we do not want to print leading spaces, having " " in Fprint inserts one space + if indent > 0 { + pattern := "" + if openTag { + // pattern would be: %4s<%s%s>%s + pattern = fmt.Sprintf("%%%ds<%%s%%s>%%s", indent) + util.Fprintf(w, pattern, " ", n.tag, attributes, eol) + } else { + // pattern would be: %4s</%s>%s + pattern = fmt.Sprintf("%%%ds</%%s>%%s", indent) + util.Fprintf(w, pattern, " ", n.tag, eol) + } + } else { + if openTag { + // pattern would be: <%s%s>%s + util.Fprintf(w, "<%s%s>%s", n.tag, attributes, eol) + } else { + // pattern would be: </%s>%s + util.Fprintf(w, "</%s>%s", n.tag, eol) + } + } +} + +// writeValue prints XML value into io.Writer +func (n *xmlNode) writeValue(w io.Writer, value string) { + util.Fprintf(w, "%s", value) +} diff --git a/pkg/vendored/argo-rollouts/README.md b/pkg/vendored/argo-rollouts/README.md new file mode 100644 index 00000000..4969dcec --- /dev/null +++ b/pkg/vendored/argo-rollouts/README.md @@ -0,0 +1,26 @@ +# Argo Rollouts API Vendored Code + +This directory contains vendored API types from the [Argo Rollouts](https://github.com/argoproj/argo-rollouts) project. + +## Source + +- **Repository**: https://github.com/argoproj/argo-rollouts +- **Version**: v1.8.3 +- **Date Vendored**: 2026-01-26 + +## Reason for Vendoring + +To have full control over the CRD API types and avoid unexpected breaking changes when the upstream operator updates. +This allows the W&B operator to manage Argo Rollouts while controlling when and how we adopt upstream changes. + +## Changes Made + +### Updated +- Update import in `argoproj.io.rollouts/v1alpha/register.go` to refer to `argoproj.io.rollouts/register.go` to avoid +upstream dependancy + +### Deleted Files + +- generated.pb.go +- generated.proto +- openai_generated.go \ No newline at end of file diff --git a/pkg/vendored/argo-rollouts/argoproj.io.rollouts/register.go b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/register.go new file mode 100644 index 00000000..b6d4a03f --- /dev/null +++ b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/register.go @@ -0,0 +1,46 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rollouts + +const ( + Group string = "argoproj.io" + + RolloutKind string = "Rollout" + RolloutSingular string = "rollout" + RolloutPlural string = "rollouts" + RolloutFullName string = RolloutPlural + "." + Group + + ExperimentKind string = "Experiment" + ExperimentSingular string = "experiment" + ExperimentPlural string = "experiments" + ExperimentFullName string = ExperimentPlural + "." + Group + + AnalysisTemplateKind string = "AnalysisTemplate" + AnalysisTemplateSingular string = "analysistemplate" + AnalysisTemplatePlural string = "analysistemplates" + AnalysisTemplateFullName string = AnalysisTemplatePlural + "." + Group + + ClusterAnalysisTemplateKind string = "ClusterAnalysisTemplate" + ClusterAnalysisTemplateSingular string = "clusteranalysistemplate" + ClusterAnalysisTemplatePlural string = "clusteranalysistemplates" + ClusterAnalysisTemplateFullName string = ClusterAnalysisTemplatePlural + "." + Group + + AnalysisRunKind string = "AnalysisRun" + AnalysisRunSingular string = "analysisrun" + AnalysisRunPlural string = "analysisruns" + AnalysisRunFullName string = AnalysisRunPlural + "." + Group +) diff --git a/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/analysis_types.go b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/analysis_types.go new file mode 100644 index 00000000..20c50f88 --- /dev/null +++ b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/analysis_types.go @@ -0,0 +1,640 @@ +package v1alpha1 + +import ( + "encoding/json" + "time" + + intstrutil "k8s.io/apimachinery/pkg/util/intstr" + + batchv1 "k8s.io/api/batch/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ClusterAnalysisTemplate holds the template for performing canary analysis +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:path=clusteranalysistemplates,shortName=cat,scope=Cluster +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time since resource was created" +type ClusterAnalysisTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Spec AnalysisTemplateSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` +} + +// AnalysisTemplateList is a list of AnalysisTemplate resources +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ClusterAnalysisTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"` + Items []ClusterAnalysisTemplate `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// AnalysisTemplate holds the template for performing canary analysis +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:path=analysistemplates,shortName=at +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time since resource was created" +type AnalysisTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Spec AnalysisTemplateSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` +} + +// AnalysisTemplateList is a list of AnalysisTemplate resources +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type AnalysisTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"` + Items []AnalysisTemplate `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// AnalysisTemplateSpec is the specification for a AnalysisTemplate resource +type AnalysisTemplateSpec struct { + // Metrics contains the list of metrics to query as part of an analysis run + // +patchMergeKey=name + // +patchStrategy=merge + Metrics []Metric `json:"metrics,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,1,rep,name=metrics"` + // Args are the list of arguments to the template + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + Args []Argument `json:"args,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=args"` + // DryRun object contains the settings for running the analysis in Dry-Run mode + // +patchMergeKey=metricName + // +patchStrategy=merge + // +optional + DryRun []DryRun `json:"dryRun,omitempty" patchStrategy:"merge" patchMergeKey:"metricName" protobuf:"bytes,3,rep,name=dryRun"` + // MeasurementRetention object contains the settings for retaining the number of measurements during the analysis + // +patchMergeKey=metricName + // +patchStrategy=merge + // +optional + MeasurementRetention []MeasurementRetention `json:"measurementRetention,omitempty" patchStrategy:"merge" patchMergeKey:"metricName" protobuf:"bytes,4,rep,name=measurementRetention"` + // Templates reference to a list of analysis templates to combine with the rest of the metrics for an AnalysisRun + // +patchMergeKey=templateName + // +patchStrategy=merge + Templates []AnalysisTemplateRef `json:"templates,omitempty" patchStrategy:"merge" patchMergeKey:"templateName" protobuf:"bytes,5,rep,name=templates"` +} + +// DurationString is a string representing a duration (e.g. 30s, 5m, 1h) +type DurationString string + +// Duration converts DurationString into a time.Duration +func (d DurationString) Duration() (time.Duration, error) { + return time.ParseDuration(string(d)) +} + +// Metric defines a metric in which to perform analysis +type Metric struct { + // Name is the name of the metric + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Interval defines an interval string (e.g. 30s, 5m, 1h) between each measurement. + // If omitted, will perform a single measurement + Interval DurationString `json:"interval,omitempty" protobuf:"bytes,2,opt,name=interval,casttype=DurationString"` + // InitialDelay how long the AnalysisRun should wait before starting this metric + InitialDelay DurationString `json:"initialDelay,omitempty" protobuf:"bytes,3,opt,name=initialDelay,casttype=DurationString"` + // Count is the number of times to run the measurement. If both interval and count are omitted, + // the effective count is 1. If only interval is specified, metric runs indefinitely. + // If count > 1, interval must be specified. + Count *intstrutil.IntOrString `json:"count,omitempty" protobuf:"bytes,4,opt,name=count"` + // SuccessCondition is an expression which determines if a measurement is considered successful + // Expression is a goevaluate expression. The keyword `result` is a variable reference to the + // value of measurement. Results can be both structured data or primitive. + // Examples: + // result > 10 + // (result.requests_made * result.requests_succeeded / 100) >= 90 + SuccessCondition string `json:"successCondition,omitempty" protobuf:"bytes,5,opt,name=successCondition"` + // FailureCondition is an expression which determines if a measurement is considered failed + // If both success and failure conditions are specified, and the measurement does not fall into + // either condition, the measurement is considered Inconclusive + FailureCondition string `json:"failureCondition,omitempty" protobuf:"bytes,6,opt,name=failureCondition"` + // FailureLimit is the maximum number of times the measurement is allowed to fail, before the + // entire metric is considered Failed (default: 0) + // -1 for making it disabled (when opting to use ConsecutiveSuccessLimit solely) + FailureLimit *intstrutil.IntOrString `json:"failureLimit,omitempty" protobuf:"bytes,7,opt,name=failureLimit"` + // InconclusiveLimit is the maximum number of times the measurement is allowed to measure + // Inconclusive, before the entire metric is considered Inconclusive (default: 0) + InconclusiveLimit *intstrutil.IntOrString `json:"inconclusiveLimit,omitempty" protobuf:"bytes,8,opt,name=inconclusiveLimit"` + // ConsecutiveErrorLimit is the maximum number of times the measurement is allowed to error in + // succession, before the metric is considered error (default: 4) + ConsecutiveErrorLimit *intstrutil.IntOrString `json:"consecutiveErrorLimit,omitempty" protobuf:"bytes,9,opt,name=consecutiveErrorLimit"` + // Provider configuration to the external system to use to verify the analysis + Provider MetricProvider `json:"provider" protobuf:"bytes,10,opt,name=provider"` + // ConsecutiveSuccessLimit is the number of consecutive times the measurement must succeed for the + // entire metric to be considered Successful (default: 0, which means it's disabled) + ConsecutiveSuccessLimit *intstrutil.IntOrString `json:"consecutiveSuccessLimit,omitempty" protobuf:"bytes,11,opt,name=consecutiveSuccessLimit"` +} + +// DryRun defines the settings for running the analysis in Dry-Run mode. +type DryRun struct { + // Name of the metric which needs to be evaluated in the Dry-Run mode. Wildcard '*' is supported and denotes all + // the available metrics. + MetricName string `json:"metricName" protobuf:"bytes,1,opt,name=metricName"` +} + +// MeasurementRetention defines the settings for retaining the number of measurements during the analysis. +type MeasurementRetention struct { + // MetricName is the name of the metric on which this retention policy should be applied. + MetricName string `json:"metricName" protobuf:"bytes,1,opt,name=metricName"` + // Limit is the maximum number of measurements to be retained for this given metric. + Limit int32 `json:"limit" protobuf:"varint,2,opt,name=limit"` +} + +// TTLStrategy defines the strategy for the time to live depending on if the analysis succeeded or failed +type TTLStrategy struct { + // SecondsAfterCompletion is the number of seconds to live after completion. + SecondsAfterCompletion *int32 `json:"secondsAfterCompletion,omitempty" protobuf:"varint,1,opt,name=secondsAfterCompletion"` + // SecondsAfterFailure is the number of seconds to live after failure. + SecondsAfterFailure *int32 `json:"secondsAfterFailure,omitempty" protobuf:"varint,2,opt,name=secondsAfterFailure"` + // SecondsAfterSuccess is the number of seconds to live after success. + SecondsAfterSuccess *int32 `json:"secondsAfterSuccess,omitempty" protobuf:"varint,3,opt,name=secondsAfterSuccess"` +} + +// EffectiveCount is the effective count based on whether or not count/interval is specified +// If neither count or interval is specified, the effective count is 1 +// If only interval is specified, metric runs indefinitely and there is no effective count (nil) +// Otherwise, it is the user specified value +func (m *Metric) EffectiveCount() *intstrutil.IntOrString { + // Need to check if type is String + if m.Count == nil || m.Count.IntValue() == 0 { + if m.Interval == "" { + one := intstrutil.FromInt(1) + return &one + } + return nil + } + return m.Count +} + +// MetricProvider which external system to use to verify the analysis +// Only one of the fields in this struct should be non-nil +type MetricProvider struct { + // Prometheus specifies the prometheus metric to query + Prometheus *PrometheusMetric `json:"prometheus,omitempty" protobuf:"bytes,1,opt,name=prometheus"` + // Kayenta specifies a Kayenta metric + Kayenta *KayentaMetric `json:"kayenta,omitempty" protobuf:"bytes,2,opt,name=kayenta"` + // Web specifies a generic HTTP web metric + Web *WebMetric `json:"web,omitempty" protobuf:"bytes,3,opt,name=web"` + // Datadog specifies a datadog metric to query + Datadog *DatadogMetric `json:"datadog,omitempty" protobuf:"bytes,4,opt,name=datadog"` + // Wavefront specifies the wavefront metric to query + Wavefront *WavefrontMetric `json:"wavefront,omitempty" protobuf:"bytes,5,opt,name=wavefront"` + // NewRelic specifies the newrelic metric to query + NewRelic *NewRelicMetric `json:"newRelic,omitempty" protobuf:"bytes,6,opt,name=newRelic"` + // Job specifies the job metric run + Job *JobMetric `json:"job,omitempty" protobuf:"bytes,7,opt,name=job"` + // CloudWatch specifies the cloudWatch metric to query + CloudWatch *CloudWatchMetric `json:"cloudWatch,omitempty" protobuf:"bytes,8,opt,name=cloudWatch"` + // Graphite specifies the Graphite metric to query + Graphite *GraphiteMetric `json:"graphite,omitempty" protobuf:"bytes,9,opt,name=graphite"` + // Influxdb specifies the influxdb metric to query + Influxdb *InfluxdbMetric `json:"influxdb,omitempty" protobuf:"bytes,10,opt,name=influxdb"` + // SkyWalking specifies the skywalking metric to query + SkyWalking *SkyWalkingMetric `json:"skywalking,omitempty" protobuf:"bytes,11,opt,name=skywalking"` + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Type=object + // Plugin specifies the hashicorp go-plugin metric to query + Plugin map[string]json.RawMessage `json:"plugin,omitempty" protobuf:"bytes,12,opt,name=plugin"` +} + +// AnalysisPhase is the overall phase of an AnalysisRun, MetricResult, or Measurement +type AnalysisPhase string + +// Possible AnalysisPhase values +const ( + AnalysisPhasePending AnalysisPhase = "Pending" + AnalysisPhaseRunning AnalysisPhase = "Running" + AnalysisPhaseSuccessful AnalysisPhase = "Successful" + AnalysisPhaseFailed AnalysisPhase = "Failed" + AnalysisPhaseError AnalysisPhase = "Error" + AnalysisPhaseInconclusive AnalysisPhase = "Inconclusive" +) + +// Completed returns whether or not the analysis status is considered completed +func (as AnalysisPhase) Completed() bool { + switch as { + case AnalysisPhaseSuccessful, AnalysisPhaseFailed, AnalysisPhaseError, AnalysisPhaseInconclusive: + return true + } + return false +} + +// Arguments to perform a prometheus range query +type PrometheusRangeQueryArgs struct { + // The start time to query in expr format e.g. now(), now() - duration("1h"), now() - duration("{{args.lookback_duration}}") + Start string `json:"start,omitempty" protobuf:"bytes,1,opt,name=start"` + // The end time to query in expr format e.g. now(), now() - duration("1h"), now() - duration("{{args.lookback_duration}}") + End string `json:"end,omitempty" protobuf:"bytes,2,opt,name=end"` + // The maximum time between two slices from the start to end (e.g. 30s, 5m, 1h). + Step DurationString `json:"step,omitempty" protobuf:"bytes,3,opt,name=step,casttype=DurationString"` +} + +// PrometheusMetric defines the prometheus query to perform canary analysis +type PrometheusMetric struct { + // Address is the HTTP address and port of the prometheus server + Address string `json:"address,omitempty" protobuf:"bytes,1,opt,name=address"` + // Query is a raw prometheus query to perform + Query string `json:"query,omitempty" protobuf:"bytes,2,opt,name=query"` + // Authentication details + // +optional + Authentication Authentication `json:"authentication,omitempty" protobuf:"bytes,3,opt,name=authentication"` + // Timeout represents the duration within which a prometheus query should complete. It is expressed in seconds. + // +optional + Timeout *int64 `json:"timeout,omitempty" protobuf:"bytes,4,opt,name=timeout"` + // Insecure skips host TLS verification + Insecure bool `json:"insecure,omitempty" protobuf:"varint,5,opt,name=insecure"` + // Headers are optional HTTP headers to use in the request + // +optional + // +patchMergeKey=key + // +patchStrategy=merge + Headers []WebMetricHeader `json:"headers,omitempty" patchStrategy:"merge" patchMergeKey:"key" protobuf:"bytes,6,opt,name=headers"` + // Arguments for prometheus + // +optional + RangeQuery *PrometheusRangeQueryArgs `json:"rangeQuery,omitempty" protobuf:"bytes,7,opt,name=rangeQuery"` +} + +// Authentication method +type Authentication struct { + // Sigv4 Config is the aws SigV4 configuration to use for SigV4 signing if using Amazon Managed Prometheus + // +optional + Sigv4 Sigv4Config `json:"sigv4,omitempty" protobuf:"bytes,1,opt,name=sigv4"` + // OAuth2 config + // +optional + OAuth2 OAuth2Config `json:"oauth2,omitempty" protobuf:"bytes,2,opt,name=oauth2"` +} + +type OAuth2Config struct { + // OAuth2 provider token URL + TokenURL string `json:"tokenUrl,omitempty" protobuf:"bytes,1,name=tokenUrl"` + // OAuth2 client ID + ClientID string `json:"clientId,omitempty" protobuf:"bytes,2,name=clientId"` + // OAuth2 client secret + ClientSecret string `json:"clientSecret,omitempty" protobuf:"bytes,3,name=clientSecret"` + // OAuth2 scopes + // +optional + Scopes []string `json:"scopes,omitempty" protobuf:"bytes,4,opt,name=scopes"` +} + +type Sigv4Config struct { + // Region is the AWS Region to sign the SigV4 Request + Region string `json:"region,omitempty" protobuf:"bytes,1,opt,name=address"` + // Profile is the Credential Profile used to sign the SigV4 Request + Profile string `json:"profile,omitempty" protobuf:"bytes,2,opt,name=profile"` + // RoleARN is the IAM role used to sign the SIgV4 Request + RoleARN string `json:"roleArn,omitempty" protobuf:"bytes,3,opt,name=roleArn"` +} + +// WavefrontMetric defines the wavefront query to perform canary analysis +type WavefrontMetric struct { + // Address is the HTTP address and port of the wavefront server + Address string `json:"address,omitempty" protobuf:"bytes,1,opt,name=address"` + // Query is a raw wavefront query to perform + Query string `json:"query,omitempty" protobuf:"bytes,2,opt,name=query"` +} + +// NewRelicMetric defines the newrelic query to perform canary analysis +type NewRelicMetric struct { + // Profile is the name of the secret holding NR account configuration + Profile string `json:"profile,omitempty" protobuf:"bytes,1,opt,name=profile"` + // Query is a raw newrelic NRQL query to perform + Query string `json:"query" protobuf:"bytes,2,opt,name=query"` + // Timeout represents the duration limit in seconds that will apply to the NRQL query + // +optional + Timeout *int64 `json:"timeout,omitempty" protobuf:"bytes,3,opt,name=timeout"` +} + +// JobMetric defines a job to run which acts as a metric +type JobMetric struct { + Metadata metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + Spec batchv1.JobSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` +} + +// GraphiteMetric defines the Graphite query to perform canary analysis +type GraphiteMetric struct { + // Address is the HTTP address and port of the Graphite server + Address string `json:"address,omitempty" protobuf:"bytes,1,opt,name=address"` + // Query is a raw Graphite query to perform + Query string `json:"query,omitempty" protobuf:"bytes,2,opt,name=query"` +} + +// InfluxdbMetric defines the InfluxDB Flux query to perform canary analysis +type InfluxdbMetric struct { + // Profile is the name of the secret holding InfluxDB account configuration + Profile string `json:"profile,omitempty" protobuf:"bytes,1,opt,name=profile"` + // Query is a raw InfluxDB flux query to perform + Query string `json:"query,omitempty" protobuf:"bytes,2,opt,name=query"` +} + +// CloudWatchMetric defines the cloudwatch query to perform canary analysis +type CloudWatchMetric struct { + Interval DurationString `json:"interval,omitempty" protobuf:"bytes,1,opt,name=interval,casttype=DurationString"` + MetricDataQueries []CloudWatchMetricDataQuery `json:"metricDataQueries" protobuf:"bytes,2,rep,name=metricDataQueries"` +} + +// CloudWatchMetricDataQuery defines the cloudwatch query +type CloudWatchMetricDataQuery struct { + Id string `json:"id,omitempty" protobuf:"bytes,1,opt,name=id"` + Expression *string `json:"expression,omitempty" protobuf:"bytes,2,opt,name=expression"` + Label *string `json:"label,omitempty" protobuf:"bytes,3,opt,name=label"` + MetricStat *CloudWatchMetricStat `json:"metricStat,omitempty" protobuf:"bytes,4,opt,name=metricStat"` + Period *intstrutil.IntOrString `json:"period,omitempty" protobuf:"varint,5,opt,name=period"` + ReturnData *bool `json:"returnData,omitempty" protobuf:"bytes,6,opt,name=returnData"` +} + +type CloudWatchMetricStat struct { + Metric CloudWatchMetricStatMetric `json:"metric,omitempty" protobuf:"bytes,1,opt,name=metric"` + Period intstrutil.IntOrString `json:"period,omitempty" protobuf:"varint,2,opt,name=period"` + Stat string `json:"stat,omitempty" protobuf:"bytes,3,opt,name=stat"` + Unit string `json:"unit,omitempty" protobuf:"bytes,4,opt,name=unit"` +} + +type CloudWatchMetricStatMetric struct { + Dimensions []CloudWatchMetricStatMetricDimension `json:"dimensions,omitempty" protobuf:"bytes,1,rep,name=dimensions"` + MetricName string `json:"metricName,omitempty" protobuf:"bytes,2,opt,name=metricName"` + Namespace *string `json:"namespace,omitempty" protobuf:"bytes,3,opt,name=namespace"` +} + +type CloudWatchMetricStatMetricDimension struct { + Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"` + Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"` +} + +// AnalysisRun is an instantiation of an AnalysisTemplate +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:path=analysisruns, shortName=ar +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="AnalysisRun status" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time since resource was created" +type AnalysisRun struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + Spec AnalysisRunSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` + Status AnalysisRunStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// AnalysisRunList is a list of AnalysisTemplate resources +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type AnalysisRunList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"` + Items []AnalysisRun `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +type SkyWalkingMetric struct { + Address string `json:"address,omitempty" protobuf:"bytes,1,opt,name=address"` + Query string `json:"query,omitempty" protobuf:"bytes,2,opt,name=query"` + Interval DurationString `json:"interval,omitempty" protobuf:"bytes,3,opt,name=interval,casttype=DurationString"` +} + +// AnalysisRunSpec is the spec for a AnalysisRun resource +type AnalysisRunSpec struct { + // Metrics contains the list of metrics to query as part of an analysis run + // +patchMergeKey=name + // +patchStrategy=merge + Metrics []Metric `json:"metrics" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,1,rep,name=metrics"` + // Args are the list of arguments used in this run + // +optional + // +patchMergeKey=name + // +patchStrategy=merge + Args []Argument `json:"args,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=args"` + // Terminate is used to prematurely stop the run (e.g. rollout completed and analysis is no longer desired) + Terminate bool `json:"terminate,omitempty" protobuf:"varint,3,opt,name=terminate"` + // DryRun object contains the settings for running the analysis in Dry-Run mode + // +patchMergeKey=metricName + // +patchStrategy=merge + // +optional + DryRun []DryRun `json:"dryRun,omitempty" patchStrategy:"merge" patchMergeKey:"metricName" protobuf:"bytes,4,rep,name=dryRun"` + // MeasurementRetention object contains the settings for retaining the number of measurements during the analysis + // +patchMergeKey=metricName + // +patchStrategy=merge + // +optional + MeasurementRetention []MeasurementRetention `json:"measurementRetention,omitempty" patchStrategy:"merge" patchMergeKey:"metricName" protobuf:"bytes,5,rep,name=measurementRetention"` + // TTLStrategy object contains the strategy for the time to live depending on if the analysis succeeded or failed + // +optional + TTLStrategy *TTLStrategy `json:"ttlStrategy,omitempty" protobuf:"bytes,6,opt,name=ttlStrategy"` +} + +// Argument is an argument to an AnalysisRun +type Argument struct { + // Name is the name of the argument + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Value is the value of the argument + // +optional + Value *string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"` + // ValueFrom is a reference to where a secret is stored. This field is one of the fields with valueFrom + // +optional + ValueFrom *ValueFrom `json:"valueFrom,omitempty" protobuf:"bytes,3,opt,name=valueFrom"` +} + +type ValueFrom struct { + // Secret is a reference to where a secret is stored. This field is one of the fields with valueFrom + // +optional + SecretKeyRef *SecretKeyRef `json:"secretKeyRef,omitempty" protobuf:"bytes,1,opt,name=secretKeyRef"` + // FieldRef is a reference to the fields in metadata which we are referencing. This field is one of the fields with + // valueFrom + // +optional + FieldRef *FieldRef `json:"fieldRef,omitempty" protobuf:"bytes,2,opt,name=fieldRef"` +} + +type SecretKeyRef struct { + // Name is the name of the secret + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Key is the key of the secret to select from. + Key string `json:"key" protobuf:"bytes,2,opt,name=key"` +} + +// AnalysisRunStatus is the status for a AnalysisRun resource +type AnalysisRunStatus struct { + // Phase is the status of the analysis run + Phase AnalysisPhase `json:"phase" protobuf:"bytes,1,opt,name=phase,casttype=AnalysisPhase"` + // Message is a message explaining current status + Message string `json:"message,omitempty" protobuf:"bytes,2,opt,name=message"` + // MetricResults contains the metrics collected during the run + MetricResults []MetricResult `json:"metricResults,omitempty" protobuf:"bytes,3,rep,name=metricResults"` + // StartedAt indicates when the analysisRun first started + StartedAt *metav1.Time `json:"startedAt,omitempty" protobuf:"bytes,4,opt,name=startedAt"` + // RunSummary contains the final results from the metric executions + RunSummary RunSummary `json:"runSummary,omitempty" protobuf:"bytes,5,opt,name=runSummary"` + // DryRunSummary contains the final results from the metric executions in the dry-run mode + DryRunSummary *RunSummary `json:"dryRunSummary,omitempty" protobuf:"bytes,6,opt,name=dryRunSummary"` + // CompletedAt indicates when the analysisRun completed + CompletedAt *metav1.Time `json:"completedAt,omitempty" protobuf:"bytes,7,opt,name=completedAt"` +} + +// RunSummary contains the final results from the metric executions +type RunSummary struct { + // This is equal to the sum of Successful, Failed, Inconclusive + Count int32 `json:"count,omitempty" protobuf:"varint,1,opt,name=count"` + // Successful is the number of times the metric was measured Successful + Successful int32 `json:"successful,omitempty" protobuf:"varint,2,opt,name=successful"` + // Failed is the number of times the metric was measured Failed + Failed int32 `json:"failed,omitempty" protobuf:"varint,3,opt,name=failed"` + // Inconclusive is the number of times the metric was measured Inconclusive + Inconclusive int32 `json:"inconclusive,omitempty" protobuf:"varint,4,opt,name=inconclusive"` + // Error is the number of times an error was encountered during measurement + Error int32 `json:"error,omitempty" protobuf:"varint,5,opt,name=error"` +} + +// MetricResult contain a list of the most recent measurements for a single metric along with +// counters on how often the measurement +type MetricResult struct { + // Name is the name of the metric + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Phase is the overall aggregate status of the metric + Phase AnalysisPhase `json:"phase" protobuf:"bytes,2,opt,name=phase,casttype=AnalysisPhase"` + // Measurements holds the most recent measurements collected for the metric + Measurements []Measurement `json:"measurements,omitempty" protobuf:"bytes,3,rep,name=measurements"` + // Message contains a message describing current condition (e.g. error messages) + Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name=message"` + // Count is the number of times the metric was measured without Error + // This is equal to the sum of Successful, Failed, Inconclusive + Count int32 `json:"count,omitempty" protobuf:"varint,5,opt,name=count"` + // Successful is the number of times the metric was measured Successful + Successful int32 `json:"successful,omitempty" protobuf:"varint,6,opt,name=successful"` + // Failed is the number of times the metric was measured Failed + Failed int32 `json:"failed,omitempty" protobuf:"varint,7,opt,name=failed"` + // Inconclusive is the number of times the metric was measured Inconclusive + Inconclusive int32 `json:"inconclusive,omitempty" protobuf:"varint,8,opt,name=inconclusive"` + // Error is the number of times an error was encountered during measurement + Error int32 `json:"error,omitempty" protobuf:"varint,9,opt,name=error"` + // ConsecutiveError is the number of times an error was encountered during measurement in succession + // Resets to zero when non-errors are encountered + ConsecutiveError int32 `json:"consecutiveError,omitempty" protobuf:"varint,10,opt,name=consecutiveError"` + // DryRun indicates whether this metric is running in a dry-run mode or not + DryRun bool `json:"dryRun,omitempty" protobuf:"varint,11,opt,name=dryRun"` + // Metadata stores additional metadata about this metric. It is used by different providers to store + // the final state which gets used while taking measurements. For example, Prometheus uses this field + // to store the final resolved query after substituting the template arguments. + Metadata map[string]string `json:"metadata,omitempty" protobuf:"bytes,12,rep,name=metadata"` + // ConsecutiveSuccess is the number of times a measurement was successful in succession + // Resets to zero when failures, inconclusive measurements, or errors are encountered + ConsecutiveSuccess int32 `json:"consecutiveSuccess,omitempty" protobuf:"varint,13,opt,name=consecutiveSuccess"` +} + +// Measurement is a point in time result value of a single metric, and the time it was measured +type Measurement struct { + // Phase is the status of this single measurement + Phase AnalysisPhase `json:"phase" protobuf:"bytes,1,opt,name=phase,casttype=AnalysisPhase"` + // Message contains a message describing current condition (e.g. error messages) + Message string `json:"message,omitempty" protobuf:"bytes,2,opt,name=message"` + // StartedAt is the timestamp in which this measurement started to be measured + StartedAt *metav1.Time `json:"startedAt,omitempty" protobuf:"bytes,3,opt,name=startedAt"` + // FinishedAt is the timestamp in which this measurement completed and value was collected + FinishedAt *metav1.Time `json:"finishedAt,omitempty" protobuf:"bytes,4,opt,name=finishedAt"` + // Value is the measured value of the metric + Value string `json:"value,omitempty" protobuf:"bytes,5,opt,name=value"` + // Metadata stores additional metadata about this metric result, used by the different providers + // (e.g. kayenta run ID, job name) + Metadata map[string]string `json:"metadata,omitempty" protobuf:"bytes,6,rep,name=metadata"` + // ResumeAt is the timestamp when the analysisRun should try to resume the measurement + ResumeAt *metav1.Time `json:"resumeAt,omitempty" protobuf:"bytes,7,opt,name=resumeAt"` +} + +type KayentaMetric struct { + Address string `json:"address" protobuf:"bytes,1,opt,name=address"` + + Application string `json:"application" protobuf:"bytes,2,opt,name=application"` + + CanaryConfigName string `json:"canaryConfigName" protobuf:"bytes,3,opt,name=canaryConfigName"` + + MetricsAccountName string `json:"metricsAccountName" protobuf:"bytes,4,opt,name=metricsAccountName"` + ConfigurationAccountName string `json:"configurationAccountName" protobuf:"bytes,5,opt,name=configurationAccountName"` + StorageAccountName string `json:"storageAccountName" protobuf:"bytes,6,opt,name=storageAccountName"` + + Threshold KayentaThreshold `json:"threshold" protobuf:"bytes,7,opt,name=threshold"` + + Scopes []KayentaScope `json:"scopes" protobuf:"bytes,8,rep,name=scopes"` +} + +type KayentaThreshold struct { + Pass int64 `json:"pass" protobuf:"varint,1,opt,name=pass"` + Marginal int64 `json:"marginal" protobuf:"varint,2,opt,name=marginal"` +} + +type KayentaScope struct { + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + ControlScope ScopeDetail `json:"controlScope" protobuf:"bytes,2,opt,name=controlScope"` + ExperimentScope ScopeDetail `json:"experimentScope" protobuf:"bytes,3,opt,name=experimentScope"` +} + +type ScopeDetail struct { + Scope string `json:"scope" protobuf:"bytes,1,opt,name=scope"` + Region string `json:"region" protobuf:"bytes,2,opt,name=region"` + Step int64 `json:"step" protobuf:"varint,3,opt,name=step"` + Start string `json:"start" protobuf:"bytes,4,opt,name=start"` + End string `json:"end" protobuf:"bytes,5,opt,name=end"` +} + +type WebMetric struct { + // Method is the method of the web metric (empty defaults to GET) + Method WebMetricMethod `json:"method,omitempty" protobuf:"bytes,1,opt,name=method"` + // URL is the address of the web metric + URL string `json:"url" protobuf:"bytes,2,opt,name=url"` + // +patchMergeKey=key + // +patchStrategy=merge + // Headers are optional HTTP headers to use in the request + Headers []WebMetricHeader `json:"headers,omitempty" patchStrategy:"merge" patchMergeKey:"key" protobuf:"bytes,3,rep,name=headers"` + // Body is the body of the web metric (must be POST/PUT) + Body string `json:"body,omitempty" protobuf:"bytes,4,opt,name=body"` + // TimeoutSeconds is the timeout for the request in seconds (default: 10) + TimeoutSeconds int64 `json:"timeoutSeconds,omitempty" protobuf:"varint,5,opt,name=timeoutSeconds"` + // JSONPath is a JSON Path to use as the result variable (default: "{$}") + JSONPath string `json:"jsonPath,omitempty" protobuf:"bytes,6,opt,name=jsonPath"` + // Insecure skips host TLS verification + Insecure bool `json:"insecure,omitempty" protobuf:"varint,7,opt,name=insecure"` + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Type=object + // JSONBody is the body of the web metric in a json format (method must be POST/PUT) + JSONBody json.RawMessage `json:"jsonBody,omitempty" protobuf:"bytes,8,opt,name=jsonBody,casttype=encoding/json.RawMessage"` + // Authentication details + // +optional + Authentication Authentication `json:"authentication,omitempty" protobuf:"bytes,9,opt,name=authentication"` +} + +// WebMetricMethod is the available HTTP methods +type WebMetricMethod string + +// Possible HTTP method values +const ( + WebMetricMethodGet WebMetricMethod = "GET" + WebMetricMethodPost WebMetricMethod = "POST" + WebMetricMethodPut WebMetricMethod = "PUT" +) + +type WebMetricHeader struct { + Key string `json:"key" protobuf:"bytes,1,opt,name=key"` + Value string `json:"value" protobuf:"bytes,2,opt,name=value"` +} + +type DatadogMetric struct { + // +kubebuilder:default="5m" + // Interval refers to the Interval time window in Datadog (default: 5m). Not to be confused with the polling rate for the metric. + Interval DurationString `json:"interval,omitempty" protobuf:"bytes,1,opt,name=interval,casttype=DurationString"` + Query string `json:"query,omitempty" protobuf:"bytes,2,opt,name=query"` + // Queries is a map of query_name_as_key: query. You can then use query_name_as_key inside Formula.Used for v2 + // +kubebuilder:validation:Type=object + Queries map[string]string `json:"queries,omitempty" protobuf:"bytes,3,opt,name=queries"` + // Formula refers to the Formula made up of the queries. Only useful with Queries. Used for v2 + Formula string `json:"formula,omitempty" protobuf:"bytes,4,opt,name=formula"` + // ApiVersion refers to the Datadog API version being used (default: v1). v1 will eventually be deprecated. + // +kubebuilder:validation:Enum=v1;v2 + // +kubebuilder:default=v1 + ApiVersion string `json:"apiVersion,omitempty" protobuf:"bytes,5,opt,name=apiVersion"` + // +kubebuilder:validation:Enum=avg;min;max;sum;last;percentile;mean;l2norm;area + // Aggregator is a type of aggregator to use for metrics-based queries (default: ""). Used for v2 + Aggregator string `json:"aggregator,omitempty" protobuf:"bytes,6,opt,name=aggregator"` + // Secret refers to the name of the secret that should be used for an analysis and should exists in the namespace where the controller is. + // +optional + SecretRef SecretRef `json:"secretRef,omitempty" protobuf:"bytes,7,opt,name=secretRef"` +} + +type SecretRef struct { + // Name refers to the name of the secret that should be used to integrate with Datadog. + Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"` + // Namespaced indicates whether the secret is in the namespace where rollouts it installed or in the namespace where the metric was found + Namespaced bool `json:"namespaced,omitempty" protobuf:"varint,2,opt,namespaced=dryRun"` +} diff --git a/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/doc.go b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/doc.go new file mode 100644 index 00000000..e338befe --- /dev/null +++ b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/doc.go @@ -0,0 +1,6 @@ +// +k8s:deepcopy-gen=package +// +groupName=argoproj.io +// +k8s:openapi-gen=true + +// Package v1alpha1 is the v1alpha1 version of the API. +package v1alpha1 diff --git a/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/experiment_types.go b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/experiment_types.go new file mode 100644 index 00000000..3151b656 --- /dev/null +++ b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/experiment_types.go @@ -0,0 +1,247 @@ +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Annotations that are labeled into the ReplicaSets that are part of an experiment +const ( + ExperimentNameAnnotationKey = "experiment.argoproj.io/name" + ExperimentTemplateNameAnnotationKey = "experiment.argoproj.io/template-name" +) + +// Experiment is a specification for an Experiment resource +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:path=experiments,shortName=exp +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase",description="Experiment status" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time since resource was created" +type Experiment struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Spec ExperimentSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` + Status ExperimentStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// ExperimentSpec is the spec for a Experiment resource +type ExperimentSpec struct { + // Templates are a list of PodSpecs that define the ReplicaSets that should be run during an experiment. + // +patchMergeKey=name + // +patchStrategy=merge + Templates []TemplateSpec `json:"templates" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,1,rep,name=templates"` + // Duration the amount of time for the experiment to run as a duration string (e.g. 30s, 5m, 1h). + // If omitted, the experiment will run indefinitely, stopped either via termination, or a failed analysis run. + // +optional + Duration DurationString `json:"duration,omitempty" protobuf:"bytes,2,opt,name=duration,casttype=DurationString"` + // ProgressDeadlineSeconds The maximum time in seconds for a experiment to + // make progress before it is considered to be failed. Argo Rollouts will + // continue to process failed experiments and a condition with a + // ProgressDeadlineExceeded reason will be surfaced in the experiment status. + // Defaults to 600s. + // +optional + ProgressDeadlineSeconds *int32 `json:"progressDeadlineSeconds,omitempty" protobuf:"varint,3,opt,name=progressDeadlineSeconds"` + // Terminate is used to prematurely stop the experiment + Terminate bool `json:"terminate,omitempty" protobuf:"varint,4,opt,name=terminate"` + // Analyses references AnalysisTemplates to run during the experiment + // +patchMergeKey=name + // +patchStrategy=merge + Analyses []ExperimentAnalysisTemplateRef `json:"analyses,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,5,rep,name=analyses"` + // ScaleDownDelaySeconds adds a delay before scaling down the Experiment. + // If omitted, the Experiment waits 30 seconds before scaling down. + // A minimum of 30 seconds is recommended to ensure IP table propagation across the nodes in + // a cluster. See https://github.com/argoproj/argo-rollouts/issues/19#issuecomment-476329960 for + // more information + // +optional + ScaleDownDelaySeconds *int32 `json:"scaleDownDelaySeconds,omitempty" protobuf:"varint,6,opt,name=scaleDownDelaySeconds"` + // DryRun object contains the settings for running the analysis in Dry-Run mode + // +patchMergeKey=metricName + // +patchStrategy=merge + // +optional + DryRun []DryRun `json:"dryRun,omitempty" patchStrategy:"merge" patchMergeKey:"metricName" protobuf:"bytes,7,rep,name=dryRun"` + // MeasurementRetention object contains the settings for retaining the number of measurements during the analysis + // +patchMergeKey=metricName + // +patchStrategy=merge + // +optional + MeasurementRetention []MeasurementRetention `json:"measurementRetention,omitempty" patchStrategy:"merge" patchMergeKey:"metricName" protobuf:"bytes,8,rep,name=measurementRetention"` + // AnalysisRunMetadata labels and annotations that will be added to the AnalysisRuns + // +optional + AnalysisRunMetadata AnalysisRunMetadata `json:"analysisRunMetadata,omitempty" protobuf:"bytes,9,opt,name=analysisRunMetadata"` +} + +type TemplateSpec struct { + // Name of the template used to identity replicaset running for this experiment + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Number of desired pods. This is a pointer to distinguish between explicit + // zero and not specified. Defaults to 1. + // +optional + Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,2,opt,name=replicas"` + // Minimum number of seconds for which a newly created pod should be ready + // without any of its container crashing, for it to be considered available. + // Defaults to 0 (pod will be considered available as soon as it is ready) + // +optional + MinReadySeconds int32 `json:"minReadySeconds,omitempty" protobuf:"varint,3,opt,name=minReadySeconds"` + // Label selector for pods. Existing ReplicaSets whose pods are + // selected by this will be the ones affected by this experiment. + // It must match the pod template's labels. Each selector must be unique to the other selectors in the other templates + Selector *metav1.LabelSelector `json:"selector" protobuf:"bytes,4,opt,name=selector"` + // Template describes the pods that will be created. + Template corev1.PodTemplateSpec `json:"template" protobuf:"bytes,5,opt,name=template"` + // TemplateService describes how a service should be generated for template + Service *TemplateService `json:"service,omitempty" protobuf:"bytes,6,opt,name=service"` +} + +type TemplateService struct { + // Name of the service generated by the experiment + Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"` +} + +type TemplateStatusCode string + +const ( + TemplateStatusProgressing TemplateStatusCode = "Progressing" + TemplateStatusRunning TemplateStatusCode = "Running" + TemplateStatusSuccessful TemplateStatusCode = "Successful" + TemplateStatusFailed TemplateStatusCode = "Failed" + TemplateStatusError TemplateStatusCode = "Error" +) + +func (ts TemplateStatusCode) Completed() bool { + switch ts { + case TemplateStatusSuccessful, TemplateStatusFailed, TemplateStatusError: + return true + } + return false +} + +// TemplateStatus is the status of a specific template of an Experiment +type TemplateStatus struct { + // Name of the template used to identity which hash to compare to the hash + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Total number of non-terminated pods targeted by this experiment (their labels match the selector). + Replicas int32 `json:"replicas" protobuf:"varint,2,opt,name=replicas"` + // Total number of non-terminated pods targeted by this experiment that have the desired template spec. + UpdatedReplicas int32 `json:"updatedReplicas" protobuf:"varint,3,opt,name=updatedReplicas"` + // Total number of ready pods targeted by this experiment. + ReadyReplicas int32 `json:"readyReplicas" protobuf:"varint,4,opt,name=readyReplicas"` + // Total number of available pods (ready for at least minReadySeconds) targeted by this experiment. + AvailableReplicas int32 `json:"availableReplicas" protobuf:"varint,5,opt,name=availableReplicas"` + // CollisionCount count of hash collisions for the Experiment. The Experiment controller uses this + // field as a collision avoidance mechanism when it needs to create the name for the + // newest ReplicaSet. + // +optional + CollisionCount *int32 `json:"collisionCount,omitempty" protobuf:"varint,6,opt,name=collisionCount"` + // Phase is the status of the ReplicaSet associated with the template + Status TemplateStatusCode `json:"status,omitempty" protobuf:"bytes,7,opt,name=status,casttype=TemplateStatusCode"` + // Message is a message explaining the current status + Message string `json:"message,omitempty" protobuf:"bytes,8,opt,name=message"` + // LastTransitionTime is the last time the replicaset transitioned, which resets the countdown + // on the ProgressDeadlineSeconds check. + LastTransitionTime *metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,9,opt,name=lastTransitionTime"` + // ServiceName is the name of the service which corresponds to this experiment + ServiceName string `json:"serviceName,omitempty" protobuf:"bytes,10,opt,name=serviceName"` + // PodTemplateHash is the value of the Replicas' PodTemplateHash + PodTemplateHash string `json:"podTemplateHash,omitempty" protobuf:"bytes,11,opt,name=podTemplateHash"` +} + +// ExperimentStatus is the status for a Experiment resource +type ExperimentStatus struct { + // Phase is the status of the experiment. Takes into consideration ReplicaSet degradations and + // AnalysisRun statuses + Phase AnalysisPhase `json:"phase,omitempty" protobuf:"bytes,1,opt,name=phase,casttype=AnalysisPhase"` + // Message is an explanation for the current status + // +optional + Message string `json:"message,omitempty" protobuf:"bytes,2,opt,name=message"` + // TemplateStatuses holds the ReplicaSet related statuses for individual templates + // +optional + TemplateStatuses []TemplateStatus `json:"templateStatuses,omitempty" protobuf:"bytes,3,rep,name=templateStatuses"` + // AvailableAt the time when all the templates become healthy and the experiment should start tracking the time to + // run for the duration of specificed in the spec. + // +optional + AvailableAt *metav1.Time `json:"availableAt,omitempty" protobuf:"bytes,4,opt,name=availableAt"` + // Conditions a list of conditions a experiment can have. + // +optional + Conditions []ExperimentCondition `json:"conditions,omitempty" protobuf:"bytes,5,rep,name=conditions"` + // AnalysisRuns tracks the status of AnalysisRuns associated with this Experiment + // +optional + AnalysisRuns []ExperimentAnalysisRunStatus `json:"analysisRuns,omitempty" protobuf:"bytes,6,rep,name=analysisRuns"` +} + +// ExperimentConditionType defines the conditions of Experiment +type ExperimentConditionType string + +// These are valid conditions of a experiment. +const ( + // InvalidExperimentSpec means the experiment has an invalid spec and will not progress until + // the spec is fixed. + InvalidExperimentSpec ExperimentConditionType = "InvalidSpec" + // ExperimentCompleted means the experiment is available, ie. the active service is pointing at a + // replicaset with the required replicas up and running for at least minReadySeconds. + ExperimentCompleted ExperimentConditionType = "Completed" + // ExperimentProgressing means the experiment is progressing. Progress for a experiment is + // considered when a new replica set is created or adopted, when pods scale + // up or old pods scale down, or when the services are updated. Progress is not estimated + // for paused experiment. + ExperimentProgressing ExperimentConditionType = "Progressing" + // ExperimentRunning means that an experiment has reached the desired state and is running for the duration + // specified in the spec + ExperimentRunning ExperimentConditionType = "Running" + // ExperimentReplicaFailure ReplicaFailure is added in a experiment when one of its pods + // fails to be created or deleted. + ExperimentReplicaFailure ExperimentConditionType = "ReplicaFailure" +) + +// ExperimentCondition describes the state of a experiment at a certain point. +type ExperimentCondition struct { + // Type of deployment condition. + Type ExperimentConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=ExperimentConditionType"` + // Phase of the condition, one of True, False, Unknown. + Status corev1.ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=k8s.io/api/core/v1.ConditionStatus"` + // The last time this condition was updated. + LastUpdateTime metav1.Time `json:"lastUpdateTime" protobuf:"bytes,3,opt,name=lastUpdateTime"` + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime" protobuf:"bytes,4,opt,name=lastTransitionTime"` + // The reason for the condition's last transition. + Reason string `json:"reason" protobuf:"bytes,5,opt,name=reason"` + // A human readable message indicating details about the transition. + Message string `json:"message" protobuf:"bytes,6,opt,name=message"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ExperimentList is a list of Experiment resources +type ExperimentList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"` + + Items []Experiment `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +type ExperimentAnalysisTemplateRef struct { + // Name is the name of the analysis + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // TemplateName reference of the AnalysisTemplate name used by the Experiment to create the run + TemplateName string `json:"templateName" protobuf:"bytes,2,opt,name=templateName"` + // Whether to look for the templateName at cluster scope or namespace scope + // +optional + ClusterScope bool `json:"clusterScope,omitempty" protobuf:"varint,3,opt,name=clusterScope"` + // Args are the arguments that will be added to the AnalysisRuns + // +optional + // +patchMergeKey=name + // +patchStrategy=merge + Args []Argument `json:"args,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,4,rep,name=args"` + // RequiredForCompletion blocks the Experiment from completing until the analysis has completed + RequiredForCompletion bool `json:"requiredForCompletion,omitempty" protobuf:"varint,5,opt,name=requiredForCompletion"` +} + +type ExperimentAnalysisRunStatus struct { + // Name is the name of the analysis + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // AnalysisRun is the name of the AnalysisRun + AnalysisRun string `json:"analysisRun" protobuf:"bytes,2,opt,name=analysisRun"` + // Phase is the status of the AnalysisRun + Phase AnalysisPhase `json:"phase" protobuf:"bytes,3,opt,name=phase,casttype=AnalysisPhase"` + // Message is a message explaining the current status + Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name=message"` +} diff --git a/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/register.go b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/register.go new file mode 100644 index 00000000..f2e440ee --- /dev/null +++ b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/register.go @@ -0,0 +1,54 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + rollouts "github.com/wandb/operator/pkg/vendored/argo-rollouts/argoproj.io.rollouts" +) + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: rollouts.Group, Version: "v1alpha1"} + +var ( + // GroupVersionResource for all rollout types + RolloutGVR = SchemeGroupVersion.WithResource("rollouts") + AnalysisRunGVR = SchemeGroupVersion.WithResource("analysisruns") + AnalysisTemplateGVR = SchemeGroupVersion.WithResource("analysistemplates") + ClusterAnalysisTemplateGVR = SchemeGroupVersion.WithResource("clusteranalysistemplates") + ExperimentGVR = SchemeGroupVersion.WithResource("experiments") +) + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &Rollout{}, + &RolloutList{}, + &Experiment{}, + &ExperimentList{}, + &AnalysisTemplate{}, + &AnalysisTemplateList{}, + &ClusterAnalysisTemplate{}, + &ClusterAnalysisTemplateList{}, + &AnalysisRun{}, + &AnalysisRunList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/types.go b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/types.go new file mode 100755 index 00000000..8886e292 --- /dev/null +++ b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/types.go @@ -0,0 +1,1193 @@ +package v1alpha1 + +import ( + "encoding/json" + fmt "fmt" + "strconv" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:path=rollouts,shortName=ro +// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.HPAReplicas,selectorpath=.status.selector +// +kubebuilder:printcolumn:name="Desired",type="integer",JSONPath=".spec.replicas",description="Number of desired pods" +// +kubebuilder:printcolumn:name="Current",type="integer",JSONPath=".status.replicas",description="Total number of non-terminated pods targeted by this rollout" +// +kubebuilder:printcolumn:name="Up-to-date",type="integer",JSONPath=".status.updatedReplicas",description="Total number of non-terminated pods targeted by this rollout that have the desired template spec" +// +kubebuilder:printcolumn:name="Available",type="integer",JSONPath=".status.availableReplicas",description="Total number of available pods (ready for at least minReadySeconds) targeted by this rollout" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time since resource was created" +// +kubebuilder:subresource:status + +// Rollout is a specification for a Rollout resource +type Rollout struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + Spec RolloutSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` + Status RolloutStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// RolloutSpec is the spec for a Rollout resource +type RolloutSpec struct { + TemplateResolvedFromRef bool `json:"-"` + SelectorResolvedFromRef bool `json:"-"` + // Number of desired pods. This is a pointer to distinguish between explicit + // zero and not specified. Defaults to 1. + // +optional + Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,1,opt,name=replicas"` + // Label selector for pods. Existing ReplicaSets whose pods are + // selected by this will be the ones affected by this rollout. + // It must match the pod template's labels. + // +optional + Selector *metav1.LabelSelector `json:"selector" protobuf:"bytes,2,opt,name=selector"` + // Template describes the pods that will be created. + // +optional + Template corev1.PodTemplateSpec `json:"template" protobuf:"bytes,3,opt,name=template"` + // WorkloadRef holds a references to a workload that provides Pod template + // +optional + WorkloadRef *ObjectRef `json:"workloadRef,omitempty" protobuf:"bytes,10,opt,name=workloadRef"` + // Minimum number of seconds for which a newly created pod should be ready + // without any of its container crashing, for it to be considered available. + // Defaults to 0 (pod will be considered available as soon as it is ready) + // +optional + MinReadySeconds int32 `json:"minReadySeconds,omitempty" protobuf:"varint,4,opt,name=minReadySeconds"` + // The window in which a rollback will be fast tracked (fully promoted) + // +optional + RollbackWindow *RollbackWindowSpec `json:"rollbackWindow,omitempty" protobuf:"bytes,13,opt,name=rollbackWindow"` + // The deployment strategy to use to replace existing pods with new ones. + // +optional + Strategy RolloutStrategy `json:"strategy" protobuf:"bytes,5,opt,name=strategy"` + // The number of old ReplicaSets to retain. If unspecified, will retain 10 old ReplicaSets + RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty" protobuf:"varint,6,opt,name=revisionHistoryLimit"` + // Paused pauses the rollout at its current step. + Paused bool `json:"paused,omitempty" protobuf:"varint,7,opt,name=paused"` + // ProgressDeadlineSeconds The maximum time in seconds for a rollout to + // make progress before it is considered to be failed. Argo Rollouts will + // continue to process failed rollouts and a condition with a + // ProgressDeadlineExceeded reason will be surfaced in the rollout status. + // Note that progress will not be estimated during the time a rollout is paused. + // Defaults to 600s. + ProgressDeadlineSeconds *int32 `json:"progressDeadlineSeconds,omitempty" protobuf:"varint,8,opt,name=progressDeadlineSeconds"` + // ProgressDeadlineAbort is whether to abort the update when ProgressDeadlineSeconds + // is exceeded. + // +optional + ProgressDeadlineAbort bool `json:"progressDeadlineAbort,omitempty" protobuf:"varint,12,opt,name=progressDeadlineAbort"` + // RestartAt indicates when all the pods of a Rollout should be restarted + RestartAt *metav1.Time `json:"restartAt,omitempty" protobuf:"bytes,9,opt,name=restartAt"` + // Analysis configuration for the analysis runs to retain + Analysis *AnalysisRunStrategy `json:"analysis,omitempty" protobuf:"bytes,11,opt,name=analysis"` +} + +func (s *RolloutSpec) SetResolvedSelector(selector *metav1.LabelSelector) { + s.SelectorResolvedFromRef = true + s.Selector = selector +} + +func (s *RolloutSpec) SetResolvedTemplate(template corev1.PodTemplateSpec) { + s.TemplateResolvedFromRef = true + s.Template = template +} + +func (s *RolloutSpec) EmptyTemplate() bool { + if len(s.Template.Labels) > 0 { + return false + } + if len(s.Template.Annotations) > 0 { + return false + } + return true +} + +func (s *RolloutSpec) MarshalJSON() ([]byte, error) { + type Alias RolloutSpec + + if s.TemplateResolvedFromRef || s.SelectorResolvedFromRef { + obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&struct { + Alias `json:",inline"` + }{ + Alias: (Alias)(*s), + }) + if err != nil { + return nil, err + } + if s.TemplateResolvedFromRef { + unstructured.RemoveNestedField(obj, "template") + } + if s.SelectorResolvedFromRef { + unstructured.RemoveNestedField(obj, "selector") + } + + return json.Marshal(obj) + } + return json.Marshal(&struct{ *Alias }{ + Alias: (*Alias)(s), + }) +} + +// ObjectRef holds a references to the Kubernetes object +type ObjectRef struct { + // API Version of the referent + APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,1,opt,name=apiVersion"` + // Kind of the referent + Kind string `json:"kind,omitempty" protobuf:"bytes,2,opt,name=kind"` + // Name of the referent + Name string `json:"name,omitempty" protobuf:"bytes,3,opt,name=name"` + // Automatically scale down deployment + ScaleDown string `json:"scaleDown,omitempty" protobuf:"bytes,4,opt,name=scaleDown"` +} + +const ( + // DefaultRolloutUniqueLabelKey is the default key of the selector that is added + // to existing ReplicaSets (and label key that is added to its pods) to prevent the existing ReplicaSets + // to select new pods (and old pods being select by new ReplicaSet). + DefaultRolloutUniqueLabelKey string = "rollouts-pod-template-hash" + // DefaultReplicaSetScaleDownDeadlineAnnotationKey is the default key attached to an old stable ReplicaSet after + // the rollout transitioned to a new version. It contains the time when the controller can scale down the RS. + DefaultReplicaSetScaleDownDeadlineAnnotationKey = "scale-down-deadline" + // ManagedByRolloutKey is the key used to indicate which rollout(s) manage a resource but doesn't own it. + ManagedByRolloutsKey = "argo-rollouts.argoproj.io/managed-by-rollouts" + // DefaultReplicaSetRestartAnnotationKey indicates that the ReplicaSet with this annotation was restarted at the + // time listed in the value + DefaultReplicaSetRestartAnnotationKey = "argo-rollouts.argoproj.io/restarted-after" + // LabelKeyControllerInstanceID is the label the controller uses for the rollout, experiment, analysis segregation + // between controllers. Controllers will only operate on objects with the same instanceID as the controller. + LabelKeyControllerInstanceID = "argo-rollouts.argoproj.io/controller-instance-id" +) + +// RolloutStrategy defines strategy to apply during next rollout +type RolloutStrategy struct { + // +optional + BlueGreen *BlueGreenStrategy `json:"blueGreen,omitempty" protobuf:"bytes,1,opt,name=blueGreen"` + // +optional + Canary *CanaryStrategy `json:"canary,omitempty" protobuf:"bytes,2,opt,name=canary"` +} + +// BlueGreenStrategy defines parameters for Blue Green deployment +type BlueGreenStrategy struct { + // Name of the service that the rollout modifies as the active service. + ActiveService string `json:"activeService" protobuf:"bytes,1,opt,name=activeService"` + // Name of the service that the rollout modifies as the preview service. + // +optional + PreviewService string `json:"previewService,omitempty" protobuf:"bytes,2,opt,name=previewService"` + // PreviewReplicaCount is the number of replicas to run for the preview stack before the + // switchover. Once the rollout is resumed the desired replicaset will be full scaled up before the switch occurs + // +optional + PreviewReplicaCount *int32 `json:"previewReplicaCount,omitempty" protobuf:"varint,3,opt,name=previewReplicaCount"` + // AutoPromotionEnabled indicates if the rollout should automatically promote the new ReplicaSet + // to the active service or enter a paused state. If not specified, the default value is true. + // +optional + AutoPromotionEnabled *bool `json:"autoPromotionEnabled,omitempty" protobuf:"varint,4,opt,name=autoPromotionEnabled"` + // AutoPromotionSeconds is a duration in seconds in which to delay auto-promotion (default: 0). + // The countdown begins after the preview ReplicaSet have reached full availability. + // This option is ignored if autoPromotionEnabled is set to false. + // +optional + AutoPromotionSeconds int32 `json:"autoPromotionSeconds,omitempty" protobuf:"varint,5,opt,name=autoPromotionSeconds"` + // MaxUnavailable The maximum number of pods that can be unavailable during a restart operation. + // Defaults to 25% of total replicas. + // +optional + MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty" protobuf:"bytes,6,opt,name=maxUnavailable"` + // ScaleDownDelaySeconds adds a delay before scaling down the previous replicaset. + // If omitted, the Rollout waits 30 seconds before scaling down the previous ReplicaSet. + // A minimum of 30 seconds is recommended to ensure IP table propagation across the nodes in + // a cluster. See https://github.com/argoproj/argo-rollouts/issues/19#issuecomment-476329960 for + // more information + // +optional + ScaleDownDelaySeconds *int32 `json:"scaleDownDelaySeconds,omitempty" protobuf:"varint,7,opt,name=scaleDownDelaySeconds"` + // ScaleDownDelayRevisionLimit limits the number of old RS that can run at one time before getting scaled down + // +optional + ScaleDownDelayRevisionLimit *int32 `json:"scaleDownDelayRevisionLimit,omitempty" protobuf:"varint,8,opt,name=scaleDownDelayRevisionLimit"` + // PrePromotionAnalysis configuration to run analysis before a selector switch + PrePromotionAnalysis *RolloutAnalysis `json:"prePromotionAnalysis,omitempty" protobuf:"bytes,9,opt,name=prePromotionAnalysis"` + // AntiAffinity enables anti-affinity rules for Blue Green deployment + // +optional + AntiAffinity *AntiAffinity `json:"antiAffinity,omitempty" protobuf:"bytes,10,opt,name=antiAffinity"` + // PostPromotionAnalysis configuration to run analysis after a selector switch + PostPromotionAnalysis *RolloutAnalysis `json:"postPromotionAnalysis,omitempty" protobuf:"bytes,11,opt,name=postPromotionAnalysis"` + // PreviewMetadata specify labels and annotations which will be attached to the preview pods for + // the duration which they act as a preview pod, and will be removed after + PreviewMetadata *PodTemplateMetadata `json:"previewMetadata,omitempty" protobuf:"bytes,12,opt,name=previewMetadata"` + // ActiveMetadata specify labels and annotations which will be attached to the active pods for + // the duration which they act as a active pod, and will be removed after + ActiveMetadata *PodTemplateMetadata `json:"activeMetadata,omitempty" protobuf:"bytes,13,opt,name=activeMetadata"` + // AbortScaleDownDelaySeconds adds a delay in second before scaling down the preview replicaset + // if update is aborted. 0 means not to scale down. + // Default is 30 second + // +optional + AbortScaleDownDelaySeconds *int32 `json:"abortScaleDownDelaySeconds,omitempty" protobuf:"varint,14,opt,name=abortScaleDownDelaySeconds"` +} + +// AntiAffinity defines which inter-pod scheduling rule to use for anti-affinity injection +type AntiAffinity struct { + // +optional + PreferredDuringSchedulingIgnoredDuringExecution *PreferredDuringSchedulingIgnoredDuringExecution `json:"preferredDuringSchedulingIgnoredDuringExecution,omitempty" protobuf:"bytes,1,opt,name=preferredDuringSchedulingIgnoredDuringExecution"` + // +optional + RequiredDuringSchedulingIgnoredDuringExecution *RequiredDuringSchedulingIgnoredDuringExecution `json:"requiredDuringSchedulingIgnoredDuringExecution,omitempty" protobuf:"bytes,2,opt,name=requiredDuringSchedulingIgnoredDuringExecution"` +} + +// PreferredDuringSchedulingIgnoredDuringExecution defines the weight of the anti-affinity injection +type PreferredDuringSchedulingIgnoredDuringExecution struct { + // Weight associated with matching the corresponding podAffinityTerm, in the range 1-100. + Weight int32 `json:"weight" protobuf:"varint,1,opt,name=weight"` +} + +// RequiredDuringSchedulingIgnoredDuringExecution defines inter-pod scheduling rule to be RequiredDuringSchedulingIgnoredDuringExecution +type RequiredDuringSchedulingIgnoredDuringExecution struct{} + +// CanaryStrategy defines parameters for a Replica Based Canary +type CanaryStrategy struct { + // CanaryService holds the name of a service which selects pods with canary version and don't select any pods with stable version. + // +optional + CanaryService string `json:"canaryService,omitempty" protobuf:"bytes,1,opt,name=canaryService"` + // StableService holds the name of a service which selects pods with stable version and don't select any pods with canary version. + // +optional + StableService string `json:"stableService,omitempty" protobuf:"bytes,2,opt,name=stableService"` + // Steps define the order of phases to execute the canary deployment + // +optional + Steps []CanaryStep `json:"steps,omitempty" protobuf:"bytes,3,rep,name=steps"` + // TrafficRouting hosts all the supported service meshes supported to enable more fine-grained traffic routing + TrafficRouting *RolloutTrafficRouting `json:"trafficRouting,omitempty" protobuf:"bytes,4,opt,name=trafficRouting"` + + // MaxUnavailable The maximum number of pods that can be unavailable during the update. + // Value can be an absolute number (ex: 5) or a percentage of total pods at the start of update (ex: 10%). + // Absolute number is calculated from percentage by rounding down. + // This can not be 0 if MaxSurge is 0. + // By default, a fixed value of 25% is used. + // Example: when this is set to 30%, the old RC can be scaled down by 30% + // immediately when the rolling update starts. Once new pods are ready, old RC + // can be scaled down further, followed by scaling up the new RC, ensuring + // that at least 70% of original number of pods are available at all times + // during the update. + // +optional + MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty" protobuf:"bytes,5,opt,name=maxUnavailable"` + + // MaxSurge The maximum number of pods that can be scheduled above the original number of + // pods. + // Value can be an absolute number (ex: 5) or a percentage of total pods at + // the start of the update (ex: 10%). This can not be 0 if MaxUnavailable is 0. + // Absolute number is calculated from percentage by rounding up. + // By default, a value of 25% is used. + // Example: when this is set to 30%, the new RC can be scaled up by 30% + // immediately when the rolling update starts. Once old pods have been killed, + // new RC can be scaled up further, ensuring that total number of pods running + // at any time during the update is at most 130% of original pods. + // +optional + MaxSurge *intstr.IntOrString `json:"maxSurge,omitempty" protobuf:"bytes,6,opt,name=maxSurge"` + // Analysis runs a separate analysisRun while all the steps execute. This is intended to be a continuous validation of the new ReplicaSet + Analysis *RolloutAnalysisBackground `json:"analysis,omitempty" protobuf:"bytes,7,opt,name=analysis"` + // AntiAffinity enables anti-affinity rules for Canary deployment + // +optional + AntiAffinity *AntiAffinity `json:"antiAffinity,omitempty" protobuf:"bytes,8,opt,name=antiAffinity"` + // CanaryMetadata specify labels and annotations which will be attached to the canary pods for + // the duration which they act as a canary, and will be removed after + CanaryMetadata *PodTemplateMetadata `json:"canaryMetadata,omitempty" protobuf:"bytes,9,opt,name=canaryMetadata"` + // StableMetadata specify labels and annotations which will be attached to the stable pods for + // the duration which they act as a canary, and will be removed after + StableMetadata *PodTemplateMetadata `json:"stableMetadata,omitempty" protobuf:"bytes,10,opt,name=stableMetadata"` + + // ScaleDownDelaySeconds adds a delay before scaling down the previous ReplicaSet when the + // canary strategy is used with traffic routing (default 30 seconds). A delay in scaling down + // the previous ReplicaSet is needed after switching the stable service selector to point to + // the new ReplicaSet, in order to give time for traffic providers to re-target the new pods. + // This value is ignored with basic, replica-weighted canary without traffic routing. + // +optional + ScaleDownDelaySeconds *int32 `json:"scaleDownDelaySeconds,omitempty" protobuf:"varint,11,opt,name=scaleDownDelaySeconds"` + // ScaleDownDelayRevisionLimit limits the number of old RS that can run at one time before getting scaled down + // +optional + ScaleDownDelayRevisionLimit *int32 `json:"scaleDownDelayRevisionLimit,omitempty" protobuf:"varint,12,opt,name=scaleDownDelayRevisionLimit"` + // AbortScaleDownDelaySeconds adds a delay in second before scaling down the canary pods when update + // is aborted for canary strategy with traffic routing (not applicable for basic canary). + // 0 means canary pods are not scaled down. + // Default is 30 seconds. + // +optional + AbortScaleDownDelaySeconds *int32 `json:"abortScaleDownDelaySeconds,omitempty" protobuf:"varint,13,opt,name=abortScaleDownDelaySeconds"` + // DynamicStableScale is a traffic routing feature which dynamically scales the stable + // ReplicaSet to minimize total pods which are running during an update. This is calculated by + // scaling down the stable as traffic is increased to canary. When disabled (the default behavior) + // the stable ReplicaSet remains fully scaled to support instantaneous aborts. + DynamicStableScale bool `json:"dynamicStableScale,omitempty" protobuf:"varint,14,opt,name=dynamicStableScale"` + // PingPongSpec holds the ping and pong services + PingPong *PingPongSpec `json:"pingPong,omitempty" protobuf:"varint,15,opt,name=pingPong"` + // Assuming the desired number of pods in a stable or canary ReplicaSet is not zero, then make sure it is at least + // MinPodsPerReplicaSet for High Availability. Only applicable for TrafficRoutedCanary + MinPodsPerReplicaSet *int32 `json:"minPodsPerReplicaSet,omitempty" protobuf:"varint,16,opt,name=minPodsPerReplicaSet"` +} + +// PingPongSpec holds the ping and pong service name. +type PingPongSpec struct { + // name of the ping service + PingService string `json:"pingService" protobuf:"bytes,1,opt,name=pingService"` + // name of the pong service + PongService string `json:"pongService" protobuf:"bytes,2,opt,name=pongService"` +} + +// AnalysisRunStrategy configuration for the analysis runs and experiments to retain +type AnalysisRunStrategy struct { + // SuccessfulRunHistoryLimit limits the number of old successful analysis runs and experiments to be retained in a history + SuccessfulRunHistoryLimit *int32 `json:"successfulRunHistoryLimit,omitempty" protobuf:"varint,1,opt,name=successfulRunHistoryLimit"` + // UnsuccessfulRunHistoryLimit limits the number of old unsuccessful analysis runs and experiments to be retained in a history. + // Stages for unsuccessful: "Error", "Failed", "Inconclusive" + UnsuccessfulRunHistoryLimit *int32 `json:"unsuccessfulRunHistoryLimit,omitempty" protobuf:"varint,2,opt,name=unsuccessfulRunHistoryLimit"` +} + +// ALBTrafficRouting configuration for ALB ingress controller to control traffic routing +type ALBTrafficRouting struct { + // Ingress refers to the name of an `Ingress` resource in the same namespace as the `Rollout` + Ingress string `json:"ingress,omitempty" protobuf:"bytes,1,opt,name=ingress"` + // ServicePort refers to the port that the Ingress action should route traffic to + ServicePort int32 `json:"servicePort" protobuf:"varint,2,opt,name=servicePort"` + // RootService references the service in the ingress to the controller should add the action to + RootService string `json:"rootService,omitempty" protobuf:"bytes,3,opt,name=rootService"` + // AnnotationPrefix has to match the configured annotation prefix on the alb ingress controller + // +optional + AnnotationPrefix string `json:"annotationPrefix,omitempty" protobuf:"bytes,4,opt,name=annotationPrefix"` + // StickinessConfig refers to the duration-based stickiness of the target groups associated with an `Ingress` + // +optional + StickinessConfig *StickinessConfig `json:"stickinessConfig,omitempty" protobuf:"bytes,5,opt,name=stickinessConfig"` + // Ingresses refers to the name of an `Ingress` resource in the same namespace as the `Rollout` in a multi ingress scenario + // +optional + Ingresses []string `json:"ingresses,omitempty" protobuf:"bytes,6,opt,name=ingresses"` +} + +type StickinessConfig struct { + Enabled bool `json:"enabled" protobuf:"varint,1,opt,name=enabled"` + DurationSeconds int64 `json:"durationSeconds" protobuf:"varint,2,opt,name=durationSeconds"` +} + +// RolloutTrafficRouting hosts all the different configuration for supported service meshes to enable more fine-grained traffic routing +type RolloutTrafficRouting struct { + // Istio holds Istio specific configuration to route traffic + Istio *IstioTrafficRouting `json:"istio,omitempty" protobuf:"bytes,1,opt,name=istio"` + // Nginx holds Nginx Ingress specific configuration to route traffic + Nginx *NginxTrafficRouting `json:"nginx,omitempty" protobuf:"bytes,2,opt,name=nginx"` + // Nginx holds ALB Ingress specific configuration to route traffic + ALB *ALBTrafficRouting `json:"alb,omitempty" protobuf:"bytes,3,opt,name=alb"` + // SMI holds TrafficSplit specific configuration to route traffic + SMI *SMITrafficRouting `json:"smi,omitempty" protobuf:"bytes,4,opt,name=smi"` + // Ambassador holds specific configuration to use Ambassador to route traffic + Ambassador *AmbassadorTrafficRouting `json:"ambassador,omitempty" protobuf:"bytes,5,opt,name=ambassador"` + // AppMesh holds specific configuration to use AppMesh to route traffic + AppMesh *AppMeshTrafficRouting `json:"appMesh,omitempty" protobuf:"bytes,6,opt,name=appMesh"` + // Traefik holds specific configuration to use Traefik to route traffic + Traefik *TraefikTrafficRouting `json:"traefik,omitempty" protobuf:"bytes,7,opt,name=traefik"` + // ManagedRoutes A list of HTTP routes that Argo Rollouts manages, the order of this array also becomes the precedence in the upstream + // traffic router. + ManagedRoutes []MangedRoutes `json:"managedRoutes,omitempty" protobuf:"bytes,8,rep,name=managedRoutes"` + // Apisix holds specific configuration to use Apisix to route traffic + Apisix *ApisixTrafficRouting `json:"apisix,omitempty" protobuf:"bytes,9,opt,name=apisix"` + + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Type=object + // Plugins holds specific configuration that traffic router plugins can use for routing traffic + Plugins map[string]json.RawMessage `json:"plugins,omitempty" protobuf:"bytes,10,opt,name=plugins"` + + // MaxTrafficWeight The total weight of traffic. If unspecified, it defaults to 100 + MaxTrafficWeight *int32 `json:"maxTrafficWeight,omitempty" protobuf:"varint,11,opt,name=maxTrafficWeight"` +} + +type MangedRoutes struct { + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + //Possibly name for future use + //canaryRoute bool +} + +// TraefikTrafficRouting defines the configuration required to use Traefik as traffic router +type TraefikTrafficRouting struct { + // TraefikServiceName refer to the name of the Traefik service used to route traffic to the service + WeightedTraefikServiceName string `json:"weightedTraefikServiceName" protobuf:"bytes,1,name=weightedTraefikServiceName"` +} + +// ApisixTrafficRouting defines the configuration required to use APISIX as traffic router +type ApisixTrafficRouting struct { + // Route references an Apisix Route to modify to shape traffic + Route *ApisixRoute `json:"route,omitempty" protobuf:"bytes,1,opt,name=route"` +} + +// ApisixRoute holds information on the APISIX Route the rollout needs to modify +type ApisixRoute struct { + // Name refer to the name of the APISIX Route used to route traffic to the service + Name string `json:"name" protobuf:"bytes,1,name=name"` + // RuleRef a list of the APISIX Route HTTP Rules used to route traffic to the service + Rules []string `json:"rules,omitempty" protobuf:"bytes,2,rep,name=rules"` +} + +// AmbassadorTrafficRouting defines the configuration required to use Ambassador as traffic +// router +type AmbassadorTrafficRouting struct { + // Mappings refer to the name of the Ambassador Mappings used to route traffic to the + // service + Mappings []string `json:"mappings" protobuf:"bytes,1,rep,name=mappings"` +} + +// SMITrafficRouting configuration for TrafficSplit Custom Resource to control traffic routing +type SMITrafficRouting struct { + // RootService holds the name of that clients use to communicate. + // +optional + RootService string `json:"rootService,omitempty" protobuf:"bytes,1,opt,name=rootService"` + // TrafficSplitName holds the name of the TrafficSplit. + // +optional + TrafficSplitName string `json:"trafficSplitName,omitempty" protobuf:"bytes,2,opt,name=trafficSplitName"` +} + +// NginxTrafficRouting configuration for Nginx ingress controller to control traffic routing +type NginxTrafficRouting struct { + // AnnotationPrefix has to match the configured annotation prefix on the nginx ingress controller + // +optional + AnnotationPrefix string `json:"annotationPrefix,omitempty" protobuf:"bytes,1,opt,name=annotationPrefix"` + // StableIngress refers to the name of an `Ingress` resource in the same namespace as the `Rollout` + StableIngress string `json:"stableIngress,omitempty" protobuf:"bytes,2,opt,name=stableIngress"` + // +optional + AdditionalIngressAnnotations map[string]string `json:"additionalIngressAnnotations,omitempty" protobuf:"bytes,3,rep,name=additionalIngressAnnotations"` + // StableIngresses refers to the names of `Ingress` resources in the same namespace as the `Rollout` in a multi ingress scenario + // +optional + StableIngresses []string `json:"stableIngresses,omitempty" protobuf:"bytes,4,rep,name=stableIngresses"` + // +optional + CanaryIngressAnnotations map[string]string `json:"canaryIngressAnnotations,omitempty" protobuf:"bytes,5,rep,name=canaryIngressAnnotations"` +} + +// IstioTrafficRouting configuration for Istio service mesh to enable fine grain configuration +type IstioTrafficRouting struct { + // VirtualService references an Istio VirtualService to modify to shape traffic + VirtualService *IstioVirtualService `json:"virtualService,omitempty" protobuf:"bytes,1,opt,name=virtualService"` + // DestinationRule references an Istio DestinationRule to modify to shape traffic + DestinationRule *IstioDestinationRule `json:"destinationRule,omitempty" protobuf:"bytes,2,opt,name=destinationRule"` + // VirtualServices references a list of Istio VirtualService to modify to shape traffic + VirtualServices []IstioVirtualService `json:"virtualServices,omitempty" protobuf:"bytes,3,opt,name=virtualServices"` +} + +// IstioVirtualService holds information on the virtual service the rollout needs to modify +type IstioVirtualService struct { + // Name holds the name of the VirtualService + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // A list of HTTP routes within VirtualService to edit. If omitted, VirtualService must have a single route of this type. + Routes []string `json:"routes,omitempty" protobuf:"bytes,2,rep,name=routes"` + // A list of TLS/HTTPS routes within VirtualService to edit. If omitted, VirtualService must have a single route of this type. + TLSRoutes []TLSRoute `json:"tlsRoutes,omitempty" protobuf:"bytes,3,rep,name=tlsRoutes"` + // A list of TCP routes within VirtualService to edit. If omitted, VirtualService must have a single route of this type. + TCPRoutes []TCPRoute `json:"tcpRoutes,omitempty" protobuf:"bytes,4,rep,name=tcpRoutes"` +} + +// TLSRoute holds the information on the virtual service's TLS/HTTPS routes that are desired to be matched for changing weights. +type TLSRoute struct { + // Port number of the TLS Route desired to be matched in the given Istio VirtualService. + Port int64 `json:"port,omitempty" protobuf:"bytes,1,opt,name=port"` + // A list of all the SNI Hosts of the TLS Route desired to be matched in the given Istio VirtualService. + SNIHosts []string `json:"sniHosts,omitempty" protobuf:"bytes,2,rep,name=sniHosts"` +} + +// TCPRoute holds the information on the virtual service's TCP routes that are desired to be matched for changing weights. +type TCPRoute struct { + // Port number of the TCP Route desired to be matched in the given Istio VirtualService. + Port int64 `json:"port,omitempty" protobuf:"bytes,1,opt,name=port"` +} + +// IstioDestinationRule is a reference to an Istio DestinationRule to modify and shape traffic +type IstioDestinationRule struct { + // Name holds the name of the DestinationRule + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // CanarySubsetName is the subset name to modify labels with canary ReplicaSet pod template hash value + CanarySubsetName string `json:"canarySubsetName" protobuf:"bytes,2,opt,name=canarySubsetName"` + // StableSubsetName is the subset name to modify labels with stable ReplicaSet pod template hash value + StableSubsetName string `json:"stableSubsetName" protobuf:"bytes,3,opt,name=stableSubsetName"` +} + +// AppMeshTrafficRouting configuration for AWS AppMesh service mesh to enable fine grain configuration +type AppMeshTrafficRouting struct { + // VirtualService references an AppMesh VirtualService and VirtualRouter to modify to shape traffic + VirtualService *AppMeshVirtualService `json:"virtualService,omitempty" protobuf:"bytes,1,opt,name=virtualService"` + // VirtualNodeGroup references an AppMesh Route targets that are formed by a set of VirtualNodes that are used to shape traffic + VirtualNodeGroup *AppMeshVirtualNodeGroup `json:"virtualNodeGroup,omitempty" protobuf:"bytes,2,opt,name=virtualNodeGroup"` +} + +// AppMeshVirtualService holds information on the virtual service the rollout needs to modify +type AppMeshVirtualService struct { + // Name is the name of virtual service + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Routes is list of HTTP routes within virtual router associated with virtual service to edit. If omitted, virtual service must have a single route of this type. + Routes []string `json:"routes,omitempty" protobuf:"bytes,2,rep,name=routes"` +} + +// AppMeshVirtualNodeGroup holds information about targets used for routing traffic to a virtual service +type AppMeshVirtualNodeGroup struct { + // CanaryVirtualNodeRef is the virtual node ref to modify labels with canary ReplicaSet pod template hash value + CanaryVirtualNodeRef *AppMeshVirtualNodeReference `json:"canaryVirtualNodeRef" protobuf:"bytes,1,opt,name=canaryVirtualNodeRef"` + // StableVirtualNodeRef is the virtual node name to modify labels with stable ReplicaSet pod template hash value + StableVirtualNodeRef *AppMeshVirtualNodeReference `json:"stableVirtualNodeRef" protobuf:"bytes,2,opt,name=stableVirtualNodeRef"` +} + +// AppMeshVirtualNodeReference holds a reference to VirtualNode.appmesh.k8s.aws +type AppMeshVirtualNodeReference struct { + // Name is the name of VirtualNode CR + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` +} + +// RolloutExperimentStep defines a template that is used to create a experiment for a step +type RolloutExperimentStep struct { + // Templates what templates that should be added to the experiment. Should be non-nil + // +patchMergeKey=name + // +patchStrategy=merge + Templates []RolloutExperimentTemplate `json:"templates" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,1,rep,name=templates"` + // Duration is a duration string (e.g. 30s, 5m, 1h) that the experiment should run for + // +optional + Duration DurationString `json:"duration,omitempty" protobuf:"bytes,2,opt,name=duration,casttype=DurationString"` + // Analyses reference which analysis templates to run with the experiment + // +patchMergeKey=name + // +patchStrategy=merge + Analyses []RolloutExperimentStepAnalysisTemplateRef `json:"analyses,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,3,rep,name=analyses"` + // DryRun object contains the settings for running the analysis in Dry-Run mode + // +patchMergeKey=metricName + // +patchStrategy=merge + // +optional + DryRun []DryRun `json:"dryRun,omitempty" patchStrategy:"merge" patchMergeKey:"metricName" protobuf:"bytes,4,rep,name=dryRun"` + // AnalysisRunMetadata labels and annotations that will be added to the AnalysisRuns + // +optional + AnalysisRunMetadata AnalysisRunMetadata `json:"analysisRunMetadata,omitempty" protobuf:"bytes,5,opt,name=analysisRunMetadata"` +} + +type RolloutExperimentStepAnalysisTemplateRef struct { + // Name is a name for this analysis template invocation + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // TemplateName reference of the AnalysisTemplate name used by the Experiment to create the run + TemplateName string `json:"templateName" protobuf:"bytes,2,opt,name=templateName"` + // Whether to look for the templateName at cluster scope or namespace scope + // +optional + ClusterScope bool `json:"clusterScope,omitempty" protobuf:"varint,3,opt,name=clusterScope"` + // Args the arguments that will be added to the AnalysisRuns + // +patchMergeKey=name + // +patchStrategy=merge + Args []AnalysisRunArgument `json:"args,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,4,rep,name=args"` + // RequiredForCompletion blocks the Experiment from completing until the analysis has completed + RequiredForCompletion bool `json:"requiredForCompletion,omitempty" protobuf:"varint,5,opt,name=requiredForCompletion"` +} + +// RolloutExperimentTemplate defines the template used to create experiments for the Rollout's experiment canary step +type RolloutExperimentTemplate struct { + // Name description of template that passed to the template + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // SpecRef indicates where the rollout should get the RS template from + SpecRef ReplicaSetSpecRef `json:"specRef" protobuf:"bytes,2,opt,name=specRef,casttype=ReplicaSetSpecRef"` + // Replicas replica count for the template + // +optional + Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,3,opt,name=replicas"` + // Metadata sets labels and annotations to use for the RS created from the template + // +optional + Metadata PodTemplateMetadata `json:"metadata,omitempty" protobuf:"bytes,4,opt,name=metadata"` + // Selector overrides the selector to be used for the template's ReplicaSet. If omitted, will + // use the same selector as the Rollout + // +optional + Selector *metav1.LabelSelector `json:"selector,omitempty" protobuf:"bytes,5,opt,name=selector"` + // Weight sets the percentage of traffic the template's replicas should receive + Weight *int32 `json:"weight,omitempty" protobuf:"varint,6,opt,name=weight"` + // Service controls the optionally generated service + Service *TemplateService `json:"service,omitempty" protobuf:"bytes,7,opt,name=service"` +} + +// PodTemplateMetadata extra labels to add to the template +type PodTemplateMetadata struct { + // Labels Additional labels to add to the experiment + // +optional + Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,1,rep,name=labels"` + // Annotations additional annotations to add to the experiment + // +optional + Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,2,rep,name=annotations"` +} + +// AnalysisRunMetadata extra labels to add to the AnalysisRun +type AnalysisRunMetadata struct { + // Labels Additional labels to add to the AnalysisRun + // +optional + Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,1,rep,name=labels"` + // Annotations additional annotations to add to the AnalysisRun + // +optional + Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,2,rep,name=annotations"` +} + +// ReplicaSetSpecRef defines which RS that the experiment's template will use. +type ReplicaSetSpecRef string + +const ( + // CanarySpecRef indicates the RS template should be pulled from the newRS's template + CanarySpecRef ReplicaSetSpecRef = "canary" + // StableSpecRef indicates the RS template should be pulled from the stableRS's template + StableSpecRef ReplicaSetSpecRef = "stable" +) + +// CanaryStep defines a step of a canary deployment. +type CanaryStep struct { + // SetWeight sets what percentage of the newRS should receive + SetWeight *int32 `json:"setWeight,omitempty" protobuf:"varint,1,opt,name=setWeight"` + // Pause freezes the rollout by setting spec.Paused to true. + // A Rollout will resume when spec.Paused is reset to false. + // +optional + Pause *RolloutPause `json:"pause,omitempty" protobuf:"bytes,2,opt,name=pause"` + // Experiment defines the experiment object that should be created + Experiment *RolloutExperimentStep `json:"experiment,omitempty" protobuf:"bytes,3,opt,name=experiment"` + // Analysis defines the AnalysisRun that will run for a step + Analysis *RolloutAnalysis `json:"analysis,omitempty" protobuf:"bytes,4,opt,name=analysis"` + // SetCanaryScale defines how to scale the newRS without changing traffic weight + // +optional + SetCanaryScale *SetCanaryScale `json:"setCanaryScale,omitempty" protobuf:"bytes,5,opt,name=setCanaryScale"` + // SetHeaderRoute defines the route with specified header name to send 100% of traffic to the canary service + // +optional + SetHeaderRoute *SetHeaderRoute `json:"setHeaderRoute,omitempty" protobuf:"bytes,6,opt,name=setHeaderRoute"` + // SetMirrorRoutes Mirrors traffic that matches rules to a particular destination + // +optional + SetMirrorRoute *SetMirrorRoute `json:"setMirrorRoute,omitempty" protobuf:"bytes,8,opt,name=setMirrorRoute"` + // Plugin defines a plugin to execute for a step + Plugin *PluginStep `json:"plugin,omitempty" protobuf:"bytes,9,opt,name=plugin"` +} + +type PluginStep struct { + // Name of the hashicorp go-plugin step to query + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Type=object + // Config is the configuration object for the specified plugin + Config json.RawMessage `json:"config,omitempty" protobuf:"bytes,2,opt,name=config"` +} + +type SetMirrorRoute struct { + // Name this is the name of the route to use for the mirroring of traffic this also needs + // to be included in the `spec.strategy.canary.trafficRouting.managedRoutes` field + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Match Contains a list of rules that if mated will mirror the traffic to the services + // +optional + Match []RouteMatch `json:"match,omitempty" protobuf:"bytes,2,opt,name=match"` + + // Services The list of services to mirror the traffic to if the method, path, headers match + //Service string `json:"service" protobuf:"bytes,3,opt,name=service"` + // Percentage What percent of the traffic that matched the rules should be mirrored + Percentage *int32 `json:"percentage,omitempty" protobuf:"varint,4,opt,name=percentage"` +} + +type RouteMatch struct { + // Method What http methods should be mirrored + // +optional + Method *StringMatch `json:"method,omitempty" protobuf:"bytes,1,opt,name=method"` + // Path What url paths should be mirrored + // +optional + Path *StringMatch `json:"path,omitempty" protobuf:"bytes,2,opt,name=path"` + // Headers What request with matching headers should be mirrored + // +optional + Headers map[string]StringMatch `json:"headers,omitempty" protobuf:"bytes,3,opt,name=headers"` +} + +// StringMatch Used to define what type of matching we will use exact, prefix, or regular expression +type StringMatch struct { + // Exact The string must match exactly + Exact string `json:"exact,omitempty" protobuf:"bytes,1,opt,name=exact"` + // Prefix The string will be prefixed matched + Prefix string `json:"prefix,omitempty" protobuf:"bytes,2,opt,name=prefix"` + // Regex The string will be regular expression matched + Regex string `json:"regex,omitempty" protobuf:"bytes,3,opt,name=regex"` +} + +// SetHeaderRoute defines the route with specified header name to send 100% of traffic to the canary service +type SetHeaderRoute struct { + // Name this is the name of the route to use for the mirroring of traffic this also needs + // to be included in the `spec.strategy.canary.trafficRouting.managedRoutes` field + Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"` + Match []HeaderRoutingMatch `json:"match,omitempty" protobuf:"bytes,2,rep,name=match"` +} + +type HeaderRoutingMatch struct { + // HeaderName the name of the request header + HeaderName string `json:"headerName" protobuf:"bytes,1,opt,name=headerName"` + // HeaderValue the value of the header + HeaderValue *StringMatch `json:"headerValue" protobuf:"bytes,2,opt,name=headerValue"` +} + +// SetCanaryScale defines how to scale the newRS without changing traffic weight +type SetCanaryScale struct { + // Weight sets the percentage of replicas the newRS should have + // +optional + Weight *int32 `json:"weight,omitempty" protobuf:"varint,1,opt,name=weight"` + // Replicas sets the number of replicas the newRS should have + // +optional + Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,2,opt,name=replicas"` + // MatchTrafficWeight cancels out previously set Replicas or Weight, effectively activating SetWeight + // +optional + MatchTrafficWeight bool `json:"matchTrafficWeight,omitempty" protobuf:"varint,3,opt,name=matchTrafficWeight"` +} + +// RolloutAnalysisBackground defines a template that is used to create a background analysisRun +type RolloutAnalysisBackground struct { + RolloutAnalysis `json:",inline" protobuf:"bytes,1,opt,name=rolloutAnalysis"` + // StartingStep indicates which step the background analysis should start on + // If not listed, controller defaults to 0 + StartingStep *int32 `json:"startingStep,omitempty" protobuf:"varint,2,opt,name=startingStep"` +} + +// RolloutAnalysis defines a template that is used to create a analysisRun +type RolloutAnalysis struct { + // Templates reference to a list of analysis templates to combine for an AnalysisRun + // +patchMergeKey=templateName + // +patchStrategy=merge + Templates []AnalysisTemplateRef `json:"templates,omitempty" patchStrategy:"merge" patchMergeKey:"templateName" protobuf:"bytes,1,rep,name=templates"` + // Args the arguments that will be added to the AnalysisRuns + // +patchMergeKey=name + // +patchStrategy=merge + Args []AnalysisRunArgument `json:"args,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=args"` + // DryRun object contains the settings for running the analysis in Dry-Run mode + // +patchMergeKey=metricName + // +patchStrategy=merge + // +optional + DryRun []DryRun `json:"dryRun,omitempty" patchStrategy:"merge" patchMergeKey:"metricName" protobuf:"bytes,3,rep,name=dryRun"` + // MeasurementRetention object contains the settings for retaining the number of measurements during the analysis + // +patchMergeKey=metricName + // +patchStrategy=merge + // +optional + MeasurementRetention []MeasurementRetention `json:"measurementRetention,omitempty" patchStrategy:"merge" patchMergeKey:"metricName" protobuf:"bytes,4,rep,name=measurementRetention"` + // AnalysisRunMetadata labels and annotations that will be added to the AnalysisRuns + // +optional + AnalysisRunMetadata AnalysisRunMetadata `json:"analysisRunMetadata,omitempty" protobuf:"bytes,5,opt,name=analysisRunMetadata"` +} + +type AnalysisTemplateRef struct { + //TemplateName name of template to use in AnalysisRun + // +optional + TemplateName string `json:"templateName" protobuf:"bytes,1,opt,name=templateName"` + // Whether to look for the templateName at cluster scope or namespace scope + // +optional + ClusterScope bool `json:"clusterScope,omitempty" protobuf:"varint,2,opt,name=clusterScope"` +} + +// AnalysisRunArgument argument to add to analysisRun +type AnalysisRunArgument struct { + // Name argument name + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Value a hardcoded value for the argument. This field is a one of field with valueFrom + Value string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"` + // ValueFrom A reference to where the value is stored. This field is a one of field with valueFrom + ValueFrom *ArgumentValueFrom `json:"valueFrom,omitempty" protobuf:"bytes,3,opt,name=valueFrom"` +} + +// ArgumentValueFrom defines references to fields within resources to grab for the value (i.e. Pod Template Hash) +type ArgumentValueFrom struct { + // PodTemplateHashValue gets the value from one of the children ReplicaSet's Pod Template Hash + PodTemplateHashValue *ValueFromPodTemplateHash `json:"podTemplateHashValue,omitempty" protobuf:"bytes,1,opt,name=podTemplateHashValue,casttype=ValueFromPodTemplateHash"` + //FieldRef + FieldRef *FieldRef `json:"fieldRef,omitempty" protobuf:"bytes,2,opt,name=fieldRef"` +} + +type FieldRef struct { + // Required: Path of the field to select in the specified API version + FieldPath string `json:"fieldPath" protobuf:"bytes,1,opt,name=fieldPath"` +} + +// ValueFromPodTemplateHash indicates which ReplicaSet pod template pod hash to use +type ValueFromPodTemplateHash string + +const ( + // Stable tells the Rollout to get the pod template hash from the stable ReplicaSet + Stable ValueFromPodTemplateHash = "Stable" + // Latest tells the Rollout to get the pod template hash from the latest ReplicaSet + Latest ValueFromPodTemplateHash = "Latest" +) + +const ( + // RolloutTypeLabel indicates how the rollout created the analysisRun + RolloutTypeLabel = "rollout-type" + // RolloutTypeStepLabel indicates that the analysisRun was created as a canary step + RolloutTypeStepLabel = "Step" + // RolloutTypeBackgroundRunLabel indicates that the analysisRun was created in Background to an execution + RolloutTypeBackgroundRunLabel = "Background" + // RolloutTypePrePromotionLabel indicates that the analysisRun was created before the active service promotion + RolloutTypePrePromotionLabel = "PrePromotion" + // RolloutTypePostPromotionLabel indicates that the analysisRun was created after the active service promotion + RolloutTypePostPromotionLabel = "PostPromotion" + // RolloutCanaryStepIndexLabel indicates which step created this analysisRun + RolloutCanaryStepIndexLabel = "step-index" +) + +// RolloutPause defines a pause stage for a rollout +type RolloutPause struct { + // Duration the amount of time to wait before moving to the next step. + // +optional + Duration *intstr.IntOrString `json:"duration,omitempty" protobuf:"bytes,1,opt,name=duration"` +} + +// DurationSeconds converts the pause duration to seconds +// If Duration is nil 0 is returned +// if Duration values is string and does not contain a valid unit -1 is returned +func (p RolloutPause) DurationSeconds() int32 { + if p.Duration != nil { + if p.Duration.Type == intstr.String { + s, err := strconv.ParseInt(p.Duration.StrVal, 10, 32) + if err != nil { + d, err := time.ParseDuration(p.Duration.StrVal) + if err != nil { + return -1 + } + return int32(d.Seconds()) + } + // special case where no unit was specified + return int32(s) + } + return p.Duration.IntVal + } + return 0 +} + +// DurationFromInt creates duration in seconds from int value +func DurationFromInt(i int) *intstr.IntOrString { + d := intstr.FromInt(i) + return &d +} + +// DurationFromString creates duration from string +// value must be a string representation of an int with optional time unit (see time.ParseDuration) +func DurationFromString(s string) *intstr.IntOrString { + d := intstr.FromString(s) + return &d +} + +// PauseReason reasons that the rollout can pause +type PauseReason string + +const ( + // PauseReasonInconclusiveAnalysis pauses rollout when rollout has an inconclusive analysis run + PauseReasonInconclusiveAnalysis PauseReason = "InconclusiveAnalysisRun" + // PauseReasonInconclusiveExperiment pauses rollout when rollout has an inconclusive experiment + PauseReasonInconclusiveExperiment PauseReason = "InconclusiveExperiment" + // PauseReasonCanaryPauseStep pause rollout for canary pause step + PauseReasonCanaryPauseStep PauseReason = "CanaryPauseStep" + // PauseReasonBlueGreenPause pause rollout before promoting rollout + PauseReasonBlueGreenPause PauseReason = "BlueGreenPause" +) + +// PauseCondition the reason for a pause and when it started +type PauseCondition struct { + Reason PauseReason `json:"reason" protobuf:"bytes,1,opt,name=reason,casttype=PauseReason"` + StartTime metav1.Time `json:"startTime" protobuf:"bytes,2,opt,name=startTime"` +} + +// RolloutPhase are a set of phases that this rollout +type RolloutPhase string + +const ( + // RolloutPhaseHealthy indicates a rollout is healthy + RolloutPhaseHealthy RolloutPhase = "Healthy" + // RolloutPhaseDegraded indicates a rollout is degraded (e.g. pod unavailability, misconfiguration) + RolloutPhaseDegraded RolloutPhase = "Degraded" + // RolloutPhaseProgressing indicates a rollout is not yet healthy but still making progress towards a healthy state + RolloutPhaseProgressing RolloutPhase = "Progressing" + // RolloutPhasePaused indicates a rollout is not yet healthy and will not make progress until unpaused + RolloutPhasePaused RolloutPhase = "Paused" +) + +// RolloutStatus is the status for a Rollout resource +type RolloutStatus struct { + // Abort cancel the current rollout progression + Abort bool `json:"abort,omitempty" protobuf:"varint,1,opt,name=abort"` + // PauseConditions is a list of reasons why rollout became automatically paused (e.g. + // CanaryPauseStep, BlueGreenPause, InconclusiveAnalysis). The items in this list are populated + // by the controller but are cleared by the user (e.g. plugin, argo-cd resume action) when they + // wish to unpause. If pause conditions is empty, but controllerPause is true, it indicates + // the user manually unpaused the Rollout + PauseConditions []PauseCondition `json:"pauseConditions,omitempty" protobuf:"bytes,2,rep,name=pauseConditions"` + // ControllerPause indicates the controller has paused the rollout. It is set to true when + // the controller adds a pause condition. This field helps to discern the scenario where a + // rollout was resumed after being paused by the controller (e.g. via the plugin). In that + // situation, the pauseConditions would have been cleared , but controllerPause would still be + // set to true. + ControllerPause bool `json:"controllerPause,omitempty" protobuf:"varint,3,opt,name=controllerPause"` + // AbortedAt indicates the controller reconciled an aborted rollout. The controller uses this to understand if + // the controller needs to do some specific work when a Rollout is aborted. For example, the reconcileAbort is used + // to indicate if the Rollout should enter an aborted state when the latest AnalysisRun is a failure, or the controller + // has already put the Rollout into an aborted and should create a new AnalysisRun. + AbortedAt *metav1.Time `json:"abortedAt,omitempty" protobuf:"bytes,4,opt,name=abortedAt"` + // CurrentPodHash the hash of the current pod template + // +optional + CurrentPodHash string `json:"currentPodHash,omitempty" protobuf:"bytes,5,opt,name=currentPodHash"` + // CurrentStepHash the hash of the current list of steps for the current strategy. This is used to detect when the + // list of current steps change + // +optional + CurrentStepHash string `json:"currentStepHash,omitempty" protobuf:"bytes,6,opt,name=currentStepHash"` + // Total number of non-terminated pods targeted by this rollout (their labels match the selector). + // +optional + Replicas int32 `json:"replicas,omitempty" protobuf:"varint,7,opt,name=replicas"` + // Total number of non-terminated pods targeted by this rollout that have the desired template spec. + // +optional + UpdatedReplicas int32 `json:"updatedReplicas,omitempty" protobuf:"varint,8,opt,name=updatedReplicas"` + // Total number of ready pods targeted by this rollout. + // +optional + ReadyReplicas int32 `json:"readyReplicas,omitempty" protobuf:"varint,9,opt,name=readyReplicas"` + // Total number of available pods (ready for at least minReadySeconds) targeted by this rollout. + // +optional + AvailableReplicas int32 `json:"availableReplicas,omitempty" protobuf:"varint,10,opt,name=availableReplicas"` + // CurrentStepIndex defines the current step of the rollout is on. If the current step index is null, the + // controller will execute the rollout. + // +optional + CurrentStepIndex *int32 `json:"currentStepIndex,omitempty" protobuf:"varint,11,opt,name=currentStepIndex"` + // Count of hash collisions for the Rollout. The Rollout controller uses this + // field as a collision avoidance mechanism when it needs to create the name for the + // newest ReplicaSet. + // +optional + CollisionCount *int32 `json:"collisionCount,omitempty" protobuf:"varint,12,opt,name=collisionCount"` + // The generation observed by the rollout controller from metadata.generation + // +optional + ObservedGeneration string `json:"observedGeneration,omitempty" protobuf:"bytes,13,opt,name=observedGeneration"` + // Conditions a list of conditions a rollout can have. + // +optional + Conditions []RolloutCondition `json:"conditions,omitempty" protobuf:"bytes,14,rep,name=conditions"` + // Canary describes the state of the canary rollout + // +optional + Canary CanaryStatus `json:"canary,omitempty" protobuf:"bytes,15,opt,name=canary"` + // BlueGreen describes the state of the bluegreen rollout + // +optional + BlueGreen BlueGreenStatus `json:"blueGreen,omitempty" protobuf:"bytes,16,opt,name=blueGreen"` + // HPAReplicas the number of non-terminated replicas that are receiving active traffic + // +optional + HPAReplicas int32 `json:"HPAReplicas,omitempty" protobuf:"varint,17,opt,name=HPAReplicas"` + // Selector that identifies the pods that are receiving active traffic + // +optional + Selector string `json:"selector,omitempty" protobuf:"bytes,18,opt,name=selector"` + // StableRS indicates the replicaset that has successfully rolled out + // +optional + StableRS string `json:"stableRS,omitempty" protobuf:"bytes,19,opt,name=stableRS"` + // RestartedAt indicates last time a Rollout was restarted + RestartedAt *metav1.Time `json:"restartedAt,omitempty" protobuf:"bytes,20,opt,name=restartedAt"` + // PromoteFull indicates if the rollout should perform a full promotion, skipping analysis and pauses. + PromoteFull bool `json:"promoteFull,omitempty" protobuf:"varint,21,opt,name=promoteFull"` + // Phase is the rollout phase. Clients should only rely on the value if status.observedGeneration equals metadata.generation + Phase RolloutPhase `json:"phase,omitempty" protobuf:"bytes,22,opt,name=phase,casttype=RolloutPhase"` + // Message provides details on why the rollout is in its current phase + Message string `json:"message,omitempty" protobuf:"bytes,23,opt,name=message"` + // The generation of referenced workload observed by the rollout controller + // +optional + WorkloadObservedGeneration string `json:"workloadObservedGeneration,omitempty" protobuf:"bytes,24,opt,name=workloadObservedGeneration"` + /// ALB keeps information regarding the ALB and TargetGroups + ALB *ALBStatus `json:"alb,omitempty" protobuf:"bytes,25,opt,name=alb"` + /// ALBs keeps information regarding multiple ALBs and TargetGroups in a multi ingress scenario + ALBs []ALBStatus `json:"albs,omitempty" protobuf:"bytes,26,opt,name=albs"` +} + +// BlueGreenStatus status fields that only pertain to the blueGreen rollout +type BlueGreenStatus struct { + // PreviewSelector indicates which replicas set the preview service is serving traffic to + // +optional + PreviewSelector string `json:"previewSelector,omitempty" protobuf:"bytes,1,opt,name=previewSelector"` + // ActiveSelector indicates which replicas set the active service is serving traffic to + // +optional + ActiveSelector string `json:"activeSelector,omitempty" protobuf:"bytes,2,opt,name=activeSelector"` + // ScaleUpPreviewCheckPoint indicates that the Replicaset receiving traffic from the preview service is ready to be scaled up after the rollout is unpaused + // +optional + ScaleUpPreviewCheckPoint bool `json:"scaleUpPreviewCheckPoint,omitempty" protobuf:"varint,3,opt,name=scaleUpPreviewCheckPoint"` + // PrePromotionAnalysisRunStatus indicates the status of the current prepromotion analysis run + PrePromotionAnalysisRunStatus *RolloutAnalysisRunStatus `json:"prePromotionAnalysisRunStatus,omitempty" protobuf:"bytes,4,opt,name=prePromotionAnalysisRunStatus"` + // PostPromotionAnalysisRunStatus indicates the status of the current post promotion analysis run + PostPromotionAnalysisRunStatus *RolloutAnalysisRunStatus `json:"postPromotionAnalysisRunStatus,omitempty" protobuf:"bytes,5,opt,name=postPromotionAnalysisRunStatus"` +} + +// CanaryStatus status fields that only pertain to the canary rollout +type CanaryStatus struct { + // CurrentStepAnalysisRunStatus indicates the status of the current step analysis run + CurrentStepAnalysisRunStatus *RolloutAnalysisRunStatus `json:"currentStepAnalysisRunStatus,omitempty" protobuf:"bytes,1,opt,name=currentStepAnalysisRunStatus"` + // CurrentBackgroundAnalysisRunStatus indicates the status of the current background analysis run + CurrentBackgroundAnalysisRunStatus *RolloutAnalysisRunStatus `json:"currentBackgroundAnalysisRunStatus,omitempty" protobuf:"bytes,2,opt,name=currentBackgroundAnalysisRunStatus"` + // CurrentExperiment indicates the running experiment + CurrentExperiment string `json:"currentExperiment,omitempty" protobuf:"bytes,3,opt,name=currentExperiment"` + // Weights records the weights which have been set on traffic provider. Only valid when using traffic routing + Weights *TrafficWeights `json:"weights,omitempty" protobuf:"bytes,4,opt,name=weights"` + // StablePingPong For the ping-pong feature holds the current stable service, ping or pong + StablePingPong PingPongType `json:"stablePingPong,omitempty" protobuf:"bytes,5,opt,name=stablePingPong"` + // StepPluginStatuses holds the status of the step plugins executed + StepPluginStatuses []StepPluginStatus `json:"stepPluginStatuses,omitempty" protobuf:"bytes,6,rep,name=stepPluginStatuses"` +} + +type PingPongType string + +const ( + PPPing PingPongType = "ping" + PPPong PingPongType = "pong" +) + +// TrafficWeights describes the current status of how traffic has been split +type TrafficWeights struct { + // Canary is the current traffic weight split to canary ReplicaSet + Canary WeightDestination `json:"canary" protobuf:"bytes,1,opt,name=canary"` + // Stable is the current traffic weight split to stable ReplicaSet + Stable WeightDestination `json:"stable" protobuf:"bytes,2,opt,name=stable"` + // Additional holds the weights split to additional ReplicaSets such as experiment ReplicaSets + Additional []WeightDestination `json:"additional,omitempty" protobuf:"bytes,3,rep,name=additional"` + // Verified is an optional indicator that the weight has been verified to have taken effect. + // This is currently only applicable to ALB traffic router + Verified *bool `json:"verified,omitempty" protobuf:"bytes,4,opt,name=verified"` +} + +type WeightDestination struct { + // Weight is an percentage of traffic being sent to this destination + Weight int32 `json:"weight" protobuf:"varint,1,opt,name=weight"` + // ServiceName is the Kubernetes service name traffic is being sent to + ServiceName string `json:"serviceName,omitempty" protobuf:"bytes,2,opt,name=serviceName"` + // PodTemplateHash is the pod template hash label for this destination + PodTemplateHash string `json:"podTemplateHash,omitempty" protobuf:"bytes,3,opt,name=podTemplateHash"` +} + +type RolloutAnalysisRunStatus struct { + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + Status AnalysisPhase `json:"status" protobuf:"bytes,2,opt,name=status,casttype=AnalysisPhase"` + Message string `json:"message,omitempty" protobuf:"bytes,3,opt,name=message"` +} + +type StepPluginStatus struct { + // Index is the matching step index of the executed plugin + Index int32 `json:"index" protobuf:"bytes,1,name=index"` + // Name is the matching step name of the executed plugin + Name string `json:"name" protobuf:"bytes,2,name=name"` + // Operation is the name of the operation that produced this status + Operation StepPluginOperation `json:"operation" protobuf:"bytes,3,name=operation,casttype=StepPluginOperation"` + // Phase is the resulting phase of the operation + Phase StepPluginPhase `json:"phase,omitempty" protobuf:"bytes,4,opt,name=phase,casttype=StepPluginPhase"` + // Message provides details on why the plugin is in its current phase + Message string `json:"message,omitempty" protobuf:"bytes,5,opt,name=message"` + // StartedAt indicates when the plugin was first called for the operation + StartedAt *metav1.Time `json:"startedAt,omitempty" protobuf:"bytes,6,name=startedAt"` + // UpdatedAt indicates when the plugin was last called for the operation + UpdatedAt *metav1.Time `json:"updatedAt,omitempty" protobuf:"bytes,7,opt,name=updatedAt"` + // FinishedAt indicates when the operation was completed + FinishedAt *metav1.Time `json:"finishedAt,omitempty" protobuf:"bytes,8,opt,name=finishedAt"` + // Backoff is a duration to wait before trying to execute the operation again if it was not completed + Backoff DurationString `json:"backoff,omitempty" protobuf:"bytes,9,opt,name=backoff,casttype=DurationString"` + // Executions is the number of time the operation was executed + Executions int32 `json:"executions,omitempty" protobuf:"varint,10,opt,name=executions"` + // Disabled indicates if the plugin is globally disabled + Disabled bool `json:"disabled,omitempty" protobuf:"bytes,11,opt,name=disabled"` + + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Type=object + // Status holds the internal status of the plugin for this operation + Status json.RawMessage `json:"status,omitempty" protobuf:"bytes,12,opt,name=status"` +} + +// StepPluginPhase is the overall phase of a StepPlugin +type StepPluginPhase string + +// Possible StepPluginPhase values +const ( + // StepPluginPhaseRunning is the phase of a step plugin when it has not completed its execution + StepPluginPhaseRunning StepPluginPhase = "Running" + // StepPluginPhaseSuccessful is the phase of a step plugin when the operation completed successfully + StepPluginPhaseSuccessful StepPluginPhase = "Successful" + // StepPluginPhaseFailed is the phase of a step plugin when the operation completed unsuccessfully + StepPluginPhaseFailed StepPluginPhase = "Failed" + // StepPluginPhaseError is the phase of a step plugin when an unexpected error prevented the completion of the operation + StepPluginPhaseError StepPluginPhase = "Error" +) + +// Validate that the object is a valid phase +func (p StepPluginPhase) Validate() error { + switch p { + case StepPluginPhaseRunning: + case StepPluginPhaseSuccessful: + case StepPluginPhaseFailed: + case StepPluginPhaseError: + default: + return fmt.Errorf("phase '%s' is not valid", p) + } + return nil +} + +// StepPluginOperation is the operation executed by a step plugin +type StepPluginOperation string + +// Possible StepPluginOperation values +const ( + // StepPluginOperationRun is the value for the Run operation + StepPluginOperationRun StepPluginOperation = "Run" + // StepPluginOperationRun is the value for the Terminate operation + StepPluginOperationTerminate StepPluginOperation = "Terminate" + // StepPluginOperationRun is the value for the Abort operation + StepPluginOperationAbort StepPluginOperation = "Abort" +) + +type ALBStatus struct { + LoadBalancer AwsResourceRef `json:"loadBalancer,omitempty" protobuf:"bytes,1,opt,name=loadBalancer"` + CanaryTargetGroup AwsResourceRef `json:"canaryTargetGroup,omitempty" protobuf:"bytes,2,opt,name=canaryTargetGroup"` + StableTargetGroup AwsResourceRef `json:"stableTargetGroup,omitempty" protobuf:"bytes,3,opt,name=stableTargetGroup"` + Ingress string `json:"ingress,omitempty" protobuf:"bytes,4,opt,name=ingress"` +} + +type AwsResourceRef struct { + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + ARN string `json:"arn" protobuf:"bytes,2,opt,name=arn"` + // FullName is the full name of the resource + // +optional + FullName string `json:"fullName" protobuf:"bytes,3,opt,name=fullName"` +} + +// RolloutConditionType defines the conditions of Rollout +type RolloutConditionType string + +// These are valid conditions of a rollout. +const ( + // InvalidSpec means the rollout has an invalid spec and will not progress until + // the spec is fixed. + InvalidSpec RolloutConditionType = "InvalidSpec" + // RolloutAvailable means the rollout is available, ie. the active service is pointing at a + // replicaset with the required replicas up and running for at least minReadySeconds. + RolloutAvailable RolloutConditionType = "Available" + // RolloutProgressing means the rollout is progressing. Progress for a rollout is + // considered when a new replica set is created or adopted, when pods scale + // up or old pods scale down, or when the services are updated. Progress is not estimated + // for paused rollouts. + RolloutProgressing RolloutConditionType = "Progressing" + // RolloutReplicaFailure ReplicaFailure is added in a deployment when one of its pods + // fails to be created or deleted. + RolloutReplicaFailure RolloutConditionType = "ReplicaFailure" + // RolloutPaused means that rollout is in a paused state. It is still progressing at this point. + RolloutPaused RolloutConditionType = "Paused" + // RolloutCompleted indicates that the rollout completed its update to the desired revision and is not in the middle + // of any update. Note that a Completed rollout could also be considered Progressing or Degraded, if its Pods become + // unavailable sometime after the update completes. + RolloutCompleted RolloutConditionType = "Completed" + // RolloutHealthy means that rollout is in a completed state and is healthy. Which means that all the pods have been updated + // and are passing their health checks and are ready to serve traffic. + RolloutHealthy RolloutConditionType = "Healthy" +) + +// RolloutCondition describes the state of a rollout at a certain point. +type RolloutCondition struct { + // Type of deployment condition. + Type RolloutConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=RolloutConditionType"` + // Phase of the condition, one of True, False, Unknown. + Status corev1.ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=k8s.io/api/core/v1.ConditionStatus"` + // The last time this condition was updated. + LastUpdateTime metav1.Time `json:"lastUpdateTime" protobuf:"bytes,3,opt,name=lastUpdateTime"` + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime" protobuf:"bytes,4,opt,name=lastTransitionTime"` + // The reason for the condition's last transition. + Reason string `json:"reason" protobuf:"bytes,5,opt,name=reason"` + // A human readable message indicating details about the transition. + Message string `json:"message" protobuf:"bytes,6,opt,name=message"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// RolloutList is a list of Rollout resources +type RolloutList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"` + + Items []Rollout `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +type RollbackWindowSpec struct { + Revisions int32 `json:"revisions,omitempty" protobuf:"varint,1,opt,name=revisions"` +} + +const ( + ScaleDownNever string = "never" + ScaleDownOnSuccess string = "onsuccess" + ScaleDownProgressively string = "progressively" +) diff --git a/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/types_test.go b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/types_test.go new file mode 100644 index 00000000..f8ef84ae --- /dev/null +++ b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/types_test.go @@ -0,0 +1,26 @@ +package v1alpha1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRolloutPauseDuration(t *testing.T) { + rp := RolloutPause{} + assert.Equal(t, int32(0), rp.DurationSeconds()) + rp.Duration = DurationFromInt(10) + assert.Equal(t, int32(10), rp.DurationSeconds()) + rp.Duration = DurationFromString("10") + assert.Equal(t, int32(10), rp.DurationSeconds()) + rp.Duration = DurationFromString("10s") + assert.Equal(t, int32(10), rp.DurationSeconds()) + rp.Duration = DurationFromString("1h") + assert.Equal(t, int32(3600), rp.DurationSeconds()) + rp.Duration = DurationFromString("1ms") + assert.Equal(t, int32(0), rp.DurationSeconds()) + rp.Duration = DurationFromString("1z") + assert.Equal(t, int32(-1), rp.DurationSeconds()) + rp.Duration = DurationFromString("20000000000") // out of int32 + assert.Equal(t, int32(-1), rp.DurationSeconds()) +} diff --git a/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/zz_generated.deepcopy.go b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..44cfd721 --- /dev/null +++ b/pkg/vendored/argo-rollouts/argoproj.io.rollouts/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,3179 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + json "encoding/json" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + intstr "k8s.io/apimachinery/pkg/util/intstr" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ALBStatus) DeepCopyInto(out *ALBStatus) { + *out = *in + out.LoadBalancer = in.LoadBalancer + out.CanaryTargetGroup = in.CanaryTargetGroup + out.StableTargetGroup = in.StableTargetGroup + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ALBStatus. +func (in *ALBStatus) DeepCopy() *ALBStatus { + if in == nil { + return nil + } + out := new(ALBStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ALBTrafficRouting) DeepCopyInto(out *ALBTrafficRouting) { + *out = *in + if in.StickinessConfig != nil { + in, out := &in.StickinessConfig, &out.StickinessConfig + *out = new(StickinessConfig) + **out = **in + } + if in.Ingresses != nil { + in, out := &in.Ingresses, &out.Ingresses + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ALBTrafficRouting. +func (in *ALBTrafficRouting) DeepCopy() *ALBTrafficRouting { + if in == nil { + return nil + } + out := new(ALBTrafficRouting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AmbassadorTrafficRouting) DeepCopyInto(out *AmbassadorTrafficRouting) { + *out = *in + if in.Mappings != nil { + in, out := &in.Mappings, &out.Mappings + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AmbassadorTrafficRouting. +func (in *AmbassadorTrafficRouting) DeepCopy() *AmbassadorTrafficRouting { + if in == nil { + return nil + } + out := new(AmbassadorTrafficRouting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnalysisRun) DeepCopyInto(out *AnalysisRun) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalysisRun. +func (in *AnalysisRun) DeepCopy() *AnalysisRun { + if in == nil { + return nil + } + out := new(AnalysisRun) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AnalysisRun) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnalysisRunArgument) DeepCopyInto(out *AnalysisRunArgument) { + *out = *in + if in.ValueFrom != nil { + in, out := &in.ValueFrom, &out.ValueFrom + *out = new(ArgumentValueFrom) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalysisRunArgument. +func (in *AnalysisRunArgument) DeepCopy() *AnalysisRunArgument { + if in == nil { + return nil + } + out := new(AnalysisRunArgument) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnalysisRunList) DeepCopyInto(out *AnalysisRunList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AnalysisRun, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalysisRunList. +func (in *AnalysisRunList) DeepCopy() *AnalysisRunList { + if in == nil { + return nil + } + out := new(AnalysisRunList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AnalysisRunList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnalysisRunMetadata) DeepCopyInto(out *AnalysisRunMetadata) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalysisRunMetadata. +func (in *AnalysisRunMetadata) DeepCopy() *AnalysisRunMetadata { + if in == nil { + return nil + } + out := new(AnalysisRunMetadata) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnalysisRunSpec) DeepCopyInto(out *AnalysisRunSpec) { + *out = *in + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = make([]Metric, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]Argument, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DryRun != nil { + in, out := &in.DryRun, &out.DryRun + *out = make([]DryRun, len(*in)) + copy(*out, *in) + } + if in.MeasurementRetention != nil { + in, out := &in.MeasurementRetention, &out.MeasurementRetention + *out = make([]MeasurementRetention, len(*in)) + copy(*out, *in) + } + if in.TTLStrategy != nil { + in, out := &in.TTLStrategy, &out.TTLStrategy + *out = new(TTLStrategy) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalysisRunSpec. +func (in *AnalysisRunSpec) DeepCopy() *AnalysisRunSpec { + if in == nil { + return nil + } + out := new(AnalysisRunSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnalysisRunStatus) DeepCopyInto(out *AnalysisRunStatus) { + *out = *in + if in.MetricResults != nil { + in, out := &in.MetricResults, &out.MetricResults + *out = make([]MetricResult, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.StartedAt != nil { + in, out := &in.StartedAt, &out.StartedAt + *out = (*in).DeepCopy() + } + out.RunSummary = in.RunSummary + if in.DryRunSummary != nil { + in, out := &in.DryRunSummary, &out.DryRunSummary + *out = new(RunSummary) + **out = **in + } + if in.CompletedAt != nil { + in, out := &in.CompletedAt, &out.CompletedAt + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalysisRunStatus. +func (in *AnalysisRunStatus) DeepCopy() *AnalysisRunStatus { + if in == nil { + return nil + } + out := new(AnalysisRunStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnalysisRunStrategy) DeepCopyInto(out *AnalysisRunStrategy) { + *out = *in + if in.SuccessfulRunHistoryLimit != nil { + in, out := &in.SuccessfulRunHistoryLimit, &out.SuccessfulRunHistoryLimit + *out = new(int32) + **out = **in + } + if in.UnsuccessfulRunHistoryLimit != nil { + in, out := &in.UnsuccessfulRunHistoryLimit, &out.UnsuccessfulRunHistoryLimit + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalysisRunStrategy. +func (in *AnalysisRunStrategy) DeepCopy() *AnalysisRunStrategy { + if in == nil { + return nil + } + out := new(AnalysisRunStrategy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnalysisTemplate) DeepCopyInto(out *AnalysisTemplate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalysisTemplate. +func (in *AnalysisTemplate) DeepCopy() *AnalysisTemplate { + if in == nil { + return nil + } + out := new(AnalysisTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AnalysisTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnalysisTemplateList) DeepCopyInto(out *AnalysisTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AnalysisTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalysisTemplateList. +func (in *AnalysisTemplateList) DeepCopy() *AnalysisTemplateList { + if in == nil { + return nil + } + out := new(AnalysisTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AnalysisTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnalysisTemplateRef) DeepCopyInto(out *AnalysisTemplateRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalysisTemplateRef. +func (in *AnalysisTemplateRef) DeepCopy() *AnalysisTemplateRef { + if in == nil { + return nil + } + out := new(AnalysisTemplateRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnalysisTemplateSpec) DeepCopyInto(out *AnalysisTemplateSpec) { + *out = *in + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = make([]Metric, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]Argument, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DryRun != nil { + in, out := &in.DryRun, &out.DryRun + *out = make([]DryRun, len(*in)) + copy(*out, *in) + } + if in.MeasurementRetention != nil { + in, out := &in.MeasurementRetention, &out.MeasurementRetention + *out = make([]MeasurementRetention, len(*in)) + copy(*out, *in) + } + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = make([]AnalysisTemplateRef, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnalysisTemplateSpec. +func (in *AnalysisTemplateSpec) DeepCopy() *AnalysisTemplateSpec { + if in == nil { + return nil + } + out := new(AnalysisTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AntiAffinity) DeepCopyInto(out *AntiAffinity) { + *out = *in + if in.PreferredDuringSchedulingIgnoredDuringExecution != nil { + in, out := &in.PreferredDuringSchedulingIgnoredDuringExecution, &out.PreferredDuringSchedulingIgnoredDuringExecution + *out = new(PreferredDuringSchedulingIgnoredDuringExecution) + **out = **in + } + if in.RequiredDuringSchedulingIgnoredDuringExecution != nil { + in, out := &in.RequiredDuringSchedulingIgnoredDuringExecution, &out.RequiredDuringSchedulingIgnoredDuringExecution + *out = new(RequiredDuringSchedulingIgnoredDuringExecution) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AntiAffinity. +func (in *AntiAffinity) DeepCopy() *AntiAffinity { + if in == nil { + return nil + } + out := new(AntiAffinity) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApisixRoute) DeepCopyInto(out *ApisixRoute) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRoute. +func (in *ApisixRoute) DeepCopy() *ApisixRoute { + if in == nil { + return nil + } + out := new(ApisixRoute) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApisixTrafficRouting) DeepCopyInto(out *ApisixTrafficRouting) { + *out = *in + if in.Route != nil { + in, out := &in.Route, &out.Route + *out = new(ApisixRoute) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixTrafficRouting. +func (in *ApisixTrafficRouting) DeepCopy() *ApisixTrafficRouting { + if in == nil { + return nil + } + out := new(ApisixTrafficRouting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppMeshTrafficRouting) DeepCopyInto(out *AppMeshTrafficRouting) { + *out = *in + if in.VirtualService != nil { + in, out := &in.VirtualService, &out.VirtualService + *out = new(AppMeshVirtualService) + (*in).DeepCopyInto(*out) + } + if in.VirtualNodeGroup != nil { + in, out := &in.VirtualNodeGroup, &out.VirtualNodeGroup + *out = new(AppMeshVirtualNodeGroup) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppMeshTrafficRouting. +func (in *AppMeshTrafficRouting) DeepCopy() *AppMeshTrafficRouting { + if in == nil { + return nil + } + out := new(AppMeshTrafficRouting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppMeshVirtualNodeGroup) DeepCopyInto(out *AppMeshVirtualNodeGroup) { + *out = *in + if in.CanaryVirtualNodeRef != nil { + in, out := &in.CanaryVirtualNodeRef, &out.CanaryVirtualNodeRef + *out = new(AppMeshVirtualNodeReference) + **out = **in + } + if in.StableVirtualNodeRef != nil { + in, out := &in.StableVirtualNodeRef, &out.StableVirtualNodeRef + *out = new(AppMeshVirtualNodeReference) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppMeshVirtualNodeGroup. +func (in *AppMeshVirtualNodeGroup) DeepCopy() *AppMeshVirtualNodeGroup { + if in == nil { + return nil + } + out := new(AppMeshVirtualNodeGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppMeshVirtualNodeReference) DeepCopyInto(out *AppMeshVirtualNodeReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppMeshVirtualNodeReference. +func (in *AppMeshVirtualNodeReference) DeepCopy() *AppMeshVirtualNodeReference { + if in == nil { + return nil + } + out := new(AppMeshVirtualNodeReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppMeshVirtualService) DeepCopyInto(out *AppMeshVirtualService) { + *out = *in + if in.Routes != nil { + in, out := &in.Routes, &out.Routes + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppMeshVirtualService. +func (in *AppMeshVirtualService) DeepCopy() *AppMeshVirtualService { + if in == nil { + return nil + } + out := new(AppMeshVirtualService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Argument) DeepCopyInto(out *Argument) { + *out = *in + if in.Value != nil { + in, out := &in.Value, &out.Value + *out = new(string) + **out = **in + } + if in.ValueFrom != nil { + in, out := &in.ValueFrom, &out.ValueFrom + *out = new(ValueFrom) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Argument. +func (in *Argument) DeepCopy() *Argument { + if in == nil { + return nil + } + out := new(Argument) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ArgumentValueFrom) DeepCopyInto(out *ArgumentValueFrom) { + *out = *in + if in.PodTemplateHashValue != nil { + in, out := &in.PodTemplateHashValue, &out.PodTemplateHashValue + *out = new(ValueFromPodTemplateHash) + **out = **in + } + if in.FieldRef != nil { + in, out := &in.FieldRef, &out.FieldRef + *out = new(FieldRef) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgumentValueFrom. +func (in *ArgumentValueFrom) DeepCopy() *ArgumentValueFrom { + if in == nil { + return nil + } + out := new(ArgumentValueFrom) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Authentication) DeepCopyInto(out *Authentication) { + *out = *in + out.Sigv4 = in.Sigv4 + in.OAuth2.DeepCopyInto(&out.OAuth2) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication. +func (in *Authentication) DeepCopy() *Authentication { + if in == nil { + return nil + } + out := new(Authentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AwsResourceRef) DeepCopyInto(out *AwsResourceRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AwsResourceRef. +func (in *AwsResourceRef) DeepCopy() *AwsResourceRef { + if in == nil { + return nil + } + out := new(AwsResourceRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BlueGreenStatus) DeepCopyInto(out *BlueGreenStatus) { + *out = *in + if in.PrePromotionAnalysisRunStatus != nil { + in, out := &in.PrePromotionAnalysisRunStatus, &out.PrePromotionAnalysisRunStatus + *out = new(RolloutAnalysisRunStatus) + **out = **in + } + if in.PostPromotionAnalysisRunStatus != nil { + in, out := &in.PostPromotionAnalysisRunStatus, &out.PostPromotionAnalysisRunStatus + *out = new(RolloutAnalysisRunStatus) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BlueGreenStatus. +func (in *BlueGreenStatus) DeepCopy() *BlueGreenStatus { + if in == nil { + return nil + } + out := new(BlueGreenStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BlueGreenStrategy) DeepCopyInto(out *BlueGreenStrategy) { + *out = *in + if in.PreviewReplicaCount != nil { + in, out := &in.PreviewReplicaCount, &out.PreviewReplicaCount + *out = new(int32) + **out = **in + } + if in.AutoPromotionEnabled != nil { + in, out := &in.AutoPromotionEnabled, &out.AutoPromotionEnabled + *out = new(bool) + **out = **in + } + if in.MaxUnavailable != nil { + in, out := &in.MaxUnavailable, &out.MaxUnavailable + *out = new(intstr.IntOrString) + **out = **in + } + if in.ScaleDownDelaySeconds != nil { + in, out := &in.ScaleDownDelaySeconds, &out.ScaleDownDelaySeconds + *out = new(int32) + **out = **in + } + if in.ScaleDownDelayRevisionLimit != nil { + in, out := &in.ScaleDownDelayRevisionLimit, &out.ScaleDownDelayRevisionLimit + *out = new(int32) + **out = **in + } + if in.PrePromotionAnalysis != nil { + in, out := &in.PrePromotionAnalysis, &out.PrePromotionAnalysis + *out = new(RolloutAnalysis) + (*in).DeepCopyInto(*out) + } + if in.AntiAffinity != nil { + in, out := &in.AntiAffinity, &out.AntiAffinity + *out = new(AntiAffinity) + (*in).DeepCopyInto(*out) + } + if in.PostPromotionAnalysis != nil { + in, out := &in.PostPromotionAnalysis, &out.PostPromotionAnalysis + *out = new(RolloutAnalysis) + (*in).DeepCopyInto(*out) + } + if in.PreviewMetadata != nil { + in, out := &in.PreviewMetadata, &out.PreviewMetadata + *out = new(PodTemplateMetadata) + (*in).DeepCopyInto(*out) + } + if in.ActiveMetadata != nil { + in, out := &in.ActiveMetadata, &out.ActiveMetadata + *out = new(PodTemplateMetadata) + (*in).DeepCopyInto(*out) + } + if in.AbortScaleDownDelaySeconds != nil { + in, out := &in.AbortScaleDownDelaySeconds, &out.AbortScaleDownDelaySeconds + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BlueGreenStrategy. +func (in *BlueGreenStrategy) DeepCopy() *BlueGreenStrategy { + if in == nil { + return nil + } + out := new(BlueGreenStrategy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CanaryStatus) DeepCopyInto(out *CanaryStatus) { + *out = *in + if in.CurrentStepAnalysisRunStatus != nil { + in, out := &in.CurrentStepAnalysisRunStatus, &out.CurrentStepAnalysisRunStatus + *out = new(RolloutAnalysisRunStatus) + **out = **in + } + if in.CurrentBackgroundAnalysisRunStatus != nil { + in, out := &in.CurrentBackgroundAnalysisRunStatus, &out.CurrentBackgroundAnalysisRunStatus + *out = new(RolloutAnalysisRunStatus) + **out = **in + } + if in.Weights != nil { + in, out := &in.Weights, &out.Weights + *out = new(TrafficWeights) + (*in).DeepCopyInto(*out) + } + if in.StepPluginStatuses != nil { + in, out := &in.StepPluginStatuses, &out.StepPluginStatuses + *out = make([]StepPluginStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CanaryStatus. +func (in *CanaryStatus) DeepCopy() *CanaryStatus { + if in == nil { + return nil + } + out := new(CanaryStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CanaryStep) DeepCopyInto(out *CanaryStep) { + *out = *in + if in.SetWeight != nil { + in, out := &in.SetWeight, &out.SetWeight + *out = new(int32) + **out = **in + } + if in.Pause != nil { + in, out := &in.Pause, &out.Pause + *out = new(RolloutPause) + (*in).DeepCopyInto(*out) + } + if in.Experiment != nil { + in, out := &in.Experiment, &out.Experiment + *out = new(RolloutExperimentStep) + (*in).DeepCopyInto(*out) + } + if in.Analysis != nil { + in, out := &in.Analysis, &out.Analysis + *out = new(RolloutAnalysis) + (*in).DeepCopyInto(*out) + } + if in.SetCanaryScale != nil { + in, out := &in.SetCanaryScale, &out.SetCanaryScale + *out = new(SetCanaryScale) + (*in).DeepCopyInto(*out) + } + if in.SetHeaderRoute != nil { + in, out := &in.SetHeaderRoute, &out.SetHeaderRoute + *out = new(SetHeaderRoute) + (*in).DeepCopyInto(*out) + } + if in.SetMirrorRoute != nil { + in, out := &in.SetMirrorRoute, &out.SetMirrorRoute + *out = new(SetMirrorRoute) + (*in).DeepCopyInto(*out) + } + if in.Plugin != nil { + in, out := &in.Plugin, &out.Plugin + *out = new(PluginStep) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CanaryStep. +func (in *CanaryStep) DeepCopy() *CanaryStep { + if in == nil { + return nil + } + out := new(CanaryStep) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CanaryStrategy) DeepCopyInto(out *CanaryStrategy) { + *out = *in + if in.Steps != nil { + in, out := &in.Steps, &out.Steps + *out = make([]CanaryStep, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.TrafficRouting != nil { + in, out := &in.TrafficRouting, &out.TrafficRouting + *out = new(RolloutTrafficRouting) + (*in).DeepCopyInto(*out) + } + if in.MaxUnavailable != nil { + in, out := &in.MaxUnavailable, &out.MaxUnavailable + *out = new(intstr.IntOrString) + **out = **in + } + if in.MaxSurge != nil { + in, out := &in.MaxSurge, &out.MaxSurge + *out = new(intstr.IntOrString) + **out = **in + } + if in.Analysis != nil { + in, out := &in.Analysis, &out.Analysis + *out = new(RolloutAnalysisBackground) + (*in).DeepCopyInto(*out) + } + if in.AntiAffinity != nil { + in, out := &in.AntiAffinity, &out.AntiAffinity + *out = new(AntiAffinity) + (*in).DeepCopyInto(*out) + } + if in.CanaryMetadata != nil { + in, out := &in.CanaryMetadata, &out.CanaryMetadata + *out = new(PodTemplateMetadata) + (*in).DeepCopyInto(*out) + } + if in.StableMetadata != nil { + in, out := &in.StableMetadata, &out.StableMetadata + *out = new(PodTemplateMetadata) + (*in).DeepCopyInto(*out) + } + if in.ScaleDownDelaySeconds != nil { + in, out := &in.ScaleDownDelaySeconds, &out.ScaleDownDelaySeconds + *out = new(int32) + **out = **in + } + if in.ScaleDownDelayRevisionLimit != nil { + in, out := &in.ScaleDownDelayRevisionLimit, &out.ScaleDownDelayRevisionLimit + *out = new(int32) + **out = **in + } + if in.AbortScaleDownDelaySeconds != nil { + in, out := &in.AbortScaleDownDelaySeconds, &out.AbortScaleDownDelaySeconds + *out = new(int32) + **out = **in + } + if in.PingPong != nil { + in, out := &in.PingPong, &out.PingPong + *out = new(PingPongSpec) + **out = **in + } + if in.MinPodsPerReplicaSet != nil { + in, out := &in.MinPodsPerReplicaSet, &out.MinPodsPerReplicaSet + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CanaryStrategy. +func (in *CanaryStrategy) DeepCopy() *CanaryStrategy { + if in == nil { + return nil + } + out := new(CanaryStrategy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudWatchMetric) DeepCopyInto(out *CloudWatchMetric) { + *out = *in + if in.MetricDataQueries != nil { + in, out := &in.MetricDataQueries, &out.MetricDataQueries + *out = make([]CloudWatchMetricDataQuery, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudWatchMetric. +func (in *CloudWatchMetric) DeepCopy() *CloudWatchMetric { + if in == nil { + return nil + } + out := new(CloudWatchMetric) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudWatchMetricDataQuery) DeepCopyInto(out *CloudWatchMetricDataQuery) { + *out = *in + if in.Expression != nil { + in, out := &in.Expression, &out.Expression + *out = new(string) + **out = **in + } + if in.Label != nil { + in, out := &in.Label, &out.Label + *out = new(string) + **out = **in + } + if in.MetricStat != nil { + in, out := &in.MetricStat, &out.MetricStat + *out = new(CloudWatchMetricStat) + (*in).DeepCopyInto(*out) + } + if in.Period != nil { + in, out := &in.Period, &out.Period + *out = new(intstr.IntOrString) + **out = **in + } + if in.ReturnData != nil { + in, out := &in.ReturnData, &out.ReturnData + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudWatchMetricDataQuery. +func (in *CloudWatchMetricDataQuery) DeepCopy() *CloudWatchMetricDataQuery { + if in == nil { + return nil + } + out := new(CloudWatchMetricDataQuery) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudWatchMetricStat) DeepCopyInto(out *CloudWatchMetricStat) { + *out = *in + in.Metric.DeepCopyInto(&out.Metric) + out.Period = in.Period + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudWatchMetricStat. +func (in *CloudWatchMetricStat) DeepCopy() *CloudWatchMetricStat { + if in == nil { + return nil + } + out := new(CloudWatchMetricStat) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudWatchMetricStatMetric) DeepCopyInto(out *CloudWatchMetricStatMetric) { + *out = *in + if in.Dimensions != nil { + in, out := &in.Dimensions, &out.Dimensions + *out = make([]CloudWatchMetricStatMetricDimension, len(*in)) + copy(*out, *in) + } + if in.Namespace != nil { + in, out := &in.Namespace, &out.Namespace + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudWatchMetricStatMetric. +func (in *CloudWatchMetricStatMetric) DeepCopy() *CloudWatchMetricStatMetric { + if in == nil { + return nil + } + out := new(CloudWatchMetricStatMetric) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudWatchMetricStatMetricDimension) DeepCopyInto(out *CloudWatchMetricStatMetricDimension) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudWatchMetricStatMetricDimension. +func (in *CloudWatchMetricStatMetricDimension) DeepCopy() *CloudWatchMetricStatMetricDimension { + if in == nil { + return nil + } + out := new(CloudWatchMetricStatMetricDimension) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterAnalysisTemplate) DeepCopyInto(out *ClusterAnalysisTemplate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterAnalysisTemplate. +func (in *ClusterAnalysisTemplate) DeepCopy() *ClusterAnalysisTemplate { + if in == nil { + return nil + } + out := new(ClusterAnalysisTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterAnalysisTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterAnalysisTemplateList) DeepCopyInto(out *ClusterAnalysisTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterAnalysisTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterAnalysisTemplateList. +func (in *ClusterAnalysisTemplateList) DeepCopy() *ClusterAnalysisTemplateList { + if in == nil { + return nil + } + out := new(ClusterAnalysisTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterAnalysisTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DatadogMetric) DeepCopyInto(out *DatadogMetric) { + *out = *in + if in.Queries != nil { + in, out := &in.Queries, &out.Queries + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + out.SecretRef = in.SecretRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatadogMetric. +func (in *DatadogMetric) DeepCopy() *DatadogMetric { + if in == nil { + return nil + } + out := new(DatadogMetric) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DryRun) DeepCopyInto(out *DryRun) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DryRun. +func (in *DryRun) DeepCopy() *DryRun { + if in == nil { + return nil + } + out := new(DryRun) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Experiment) DeepCopyInto(out *Experiment) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Experiment. +func (in *Experiment) DeepCopy() *Experiment { + if in == nil { + return nil + } + out := new(Experiment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Experiment) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExperimentAnalysisRunStatus) DeepCopyInto(out *ExperimentAnalysisRunStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExperimentAnalysisRunStatus. +func (in *ExperimentAnalysisRunStatus) DeepCopy() *ExperimentAnalysisRunStatus { + if in == nil { + return nil + } + out := new(ExperimentAnalysisRunStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExperimentAnalysisTemplateRef) DeepCopyInto(out *ExperimentAnalysisTemplateRef) { + *out = *in + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]Argument, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExperimentAnalysisTemplateRef. +func (in *ExperimentAnalysisTemplateRef) DeepCopy() *ExperimentAnalysisTemplateRef { + if in == nil { + return nil + } + out := new(ExperimentAnalysisTemplateRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExperimentCondition) DeepCopyInto(out *ExperimentCondition) { + *out = *in + in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExperimentCondition. +func (in *ExperimentCondition) DeepCopy() *ExperimentCondition { + if in == nil { + return nil + } + out := new(ExperimentCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExperimentList) DeepCopyInto(out *ExperimentList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Experiment, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExperimentList. +func (in *ExperimentList) DeepCopy() *ExperimentList { + if in == nil { + return nil + } + out := new(ExperimentList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ExperimentList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExperimentSpec) DeepCopyInto(out *ExperimentSpec) { + *out = *in + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = make([]TemplateSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ProgressDeadlineSeconds != nil { + in, out := &in.ProgressDeadlineSeconds, &out.ProgressDeadlineSeconds + *out = new(int32) + **out = **in + } + if in.Analyses != nil { + in, out := &in.Analyses, &out.Analyses + *out = make([]ExperimentAnalysisTemplateRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ScaleDownDelaySeconds != nil { + in, out := &in.ScaleDownDelaySeconds, &out.ScaleDownDelaySeconds + *out = new(int32) + **out = **in + } + if in.DryRun != nil { + in, out := &in.DryRun, &out.DryRun + *out = make([]DryRun, len(*in)) + copy(*out, *in) + } + if in.MeasurementRetention != nil { + in, out := &in.MeasurementRetention, &out.MeasurementRetention + *out = make([]MeasurementRetention, len(*in)) + copy(*out, *in) + } + in.AnalysisRunMetadata.DeepCopyInto(&out.AnalysisRunMetadata) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExperimentSpec. +func (in *ExperimentSpec) DeepCopy() *ExperimentSpec { + if in == nil { + return nil + } + out := new(ExperimentSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExperimentStatus) DeepCopyInto(out *ExperimentStatus) { + *out = *in + if in.TemplateStatuses != nil { + in, out := &in.TemplateStatuses, &out.TemplateStatuses + *out = make([]TemplateStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AvailableAt != nil { + in, out := &in.AvailableAt, &out.AvailableAt + *out = (*in).DeepCopy() + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]ExperimentCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AnalysisRuns != nil { + in, out := &in.AnalysisRuns, &out.AnalysisRuns + *out = make([]ExperimentAnalysisRunStatus, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExperimentStatus. +func (in *ExperimentStatus) DeepCopy() *ExperimentStatus { + if in == nil { + return nil + } + out := new(ExperimentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FieldRef) DeepCopyInto(out *FieldRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FieldRef. +func (in *FieldRef) DeepCopy() *FieldRef { + if in == nil { + return nil + } + out := new(FieldRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GraphiteMetric) DeepCopyInto(out *GraphiteMetric) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GraphiteMetric. +func (in *GraphiteMetric) DeepCopy() *GraphiteMetric { + if in == nil { + return nil + } + out := new(GraphiteMetric) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HeaderRoutingMatch) DeepCopyInto(out *HeaderRoutingMatch) { + *out = *in + if in.HeaderValue != nil { + in, out := &in.HeaderValue, &out.HeaderValue + *out = new(StringMatch) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HeaderRoutingMatch. +func (in *HeaderRoutingMatch) DeepCopy() *HeaderRoutingMatch { + if in == nil { + return nil + } + out := new(HeaderRoutingMatch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfluxdbMetric) DeepCopyInto(out *InfluxdbMetric) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfluxdbMetric. +func (in *InfluxdbMetric) DeepCopy() *InfluxdbMetric { + if in == nil { + return nil + } + out := new(InfluxdbMetric) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IstioDestinationRule) DeepCopyInto(out *IstioDestinationRule) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IstioDestinationRule. +func (in *IstioDestinationRule) DeepCopy() *IstioDestinationRule { + if in == nil { + return nil + } + out := new(IstioDestinationRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IstioTrafficRouting) DeepCopyInto(out *IstioTrafficRouting) { + *out = *in + if in.VirtualService != nil { + in, out := &in.VirtualService, &out.VirtualService + *out = new(IstioVirtualService) + (*in).DeepCopyInto(*out) + } + if in.DestinationRule != nil { + in, out := &in.DestinationRule, &out.DestinationRule + *out = new(IstioDestinationRule) + **out = **in + } + if in.VirtualServices != nil { + in, out := &in.VirtualServices, &out.VirtualServices + *out = make([]IstioVirtualService, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IstioTrafficRouting. +func (in *IstioTrafficRouting) DeepCopy() *IstioTrafficRouting { + if in == nil { + return nil + } + out := new(IstioTrafficRouting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IstioVirtualService) DeepCopyInto(out *IstioVirtualService) { + *out = *in + if in.Routes != nil { + in, out := &in.Routes, &out.Routes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TLSRoutes != nil { + in, out := &in.TLSRoutes, &out.TLSRoutes + *out = make([]TLSRoute, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.TCPRoutes != nil { + in, out := &in.TCPRoutes, &out.TCPRoutes + *out = make([]TCPRoute, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IstioVirtualService. +func (in *IstioVirtualService) DeepCopy() *IstioVirtualService { + if in == nil { + return nil + } + out := new(IstioVirtualService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JobMetric) DeepCopyInto(out *JobMetric) { + *out = *in + in.Metadata.DeepCopyInto(&out.Metadata) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobMetric. +func (in *JobMetric) DeepCopy() *JobMetric { + if in == nil { + return nil + } + out := new(JobMetric) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KayentaMetric) DeepCopyInto(out *KayentaMetric) { + *out = *in + out.Threshold = in.Threshold + if in.Scopes != nil { + in, out := &in.Scopes, &out.Scopes + *out = make([]KayentaScope, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KayentaMetric. +func (in *KayentaMetric) DeepCopy() *KayentaMetric { + if in == nil { + return nil + } + out := new(KayentaMetric) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KayentaScope) DeepCopyInto(out *KayentaScope) { + *out = *in + out.ControlScope = in.ControlScope + out.ExperimentScope = in.ExperimentScope + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KayentaScope. +func (in *KayentaScope) DeepCopy() *KayentaScope { + if in == nil { + return nil + } + out := new(KayentaScope) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KayentaThreshold) DeepCopyInto(out *KayentaThreshold) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KayentaThreshold. +func (in *KayentaThreshold) DeepCopy() *KayentaThreshold { + if in == nil { + return nil + } + out := new(KayentaThreshold) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MangedRoutes) DeepCopyInto(out *MangedRoutes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MangedRoutes. +func (in *MangedRoutes) DeepCopy() *MangedRoutes { + if in == nil { + return nil + } + out := new(MangedRoutes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Measurement) DeepCopyInto(out *Measurement) { + *out = *in + if in.StartedAt != nil { + in, out := &in.StartedAt, &out.StartedAt + *out = (*in).DeepCopy() + } + if in.FinishedAt != nil { + in, out := &in.FinishedAt, &out.FinishedAt + *out = (*in).DeepCopy() + } + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ResumeAt != nil { + in, out := &in.ResumeAt, &out.ResumeAt + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Measurement. +func (in *Measurement) DeepCopy() *Measurement { + if in == nil { + return nil + } + out := new(Measurement) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MeasurementRetention) DeepCopyInto(out *MeasurementRetention) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MeasurementRetention. +func (in *MeasurementRetention) DeepCopy() *MeasurementRetention { + if in == nil { + return nil + } + out := new(MeasurementRetention) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metric) DeepCopyInto(out *Metric) { + *out = *in + if in.Count != nil { + in, out := &in.Count, &out.Count + *out = new(intstr.IntOrString) + **out = **in + } + if in.FailureLimit != nil { + in, out := &in.FailureLimit, &out.FailureLimit + *out = new(intstr.IntOrString) + **out = **in + } + if in.InconclusiveLimit != nil { + in, out := &in.InconclusiveLimit, &out.InconclusiveLimit + *out = new(intstr.IntOrString) + **out = **in + } + if in.ConsecutiveErrorLimit != nil { + in, out := &in.ConsecutiveErrorLimit, &out.ConsecutiveErrorLimit + *out = new(intstr.IntOrString) + **out = **in + } + in.Provider.DeepCopyInto(&out.Provider) + if in.ConsecutiveSuccessLimit != nil { + in, out := &in.ConsecutiveSuccessLimit, &out.ConsecutiveSuccessLimit + *out = new(intstr.IntOrString) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metric. +func (in *Metric) DeepCopy() *Metric { + if in == nil { + return nil + } + out := new(Metric) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MetricProvider) DeepCopyInto(out *MetricProvider) { + *out = *in + if in.Prometheus != nil { + in, out := &in.Prometheus, &out.Prometheus + *out = new(PrometheusMetric) + (*in).DeepCopyInto(*out) + } + if in.Kayenta != nil { + in, out := &in.Kayenta, &out.Kayenta + *out = new(KayentaMetric) + (*in).DeepCopyInto(*out) + } + if in.Web != nil { + in, out := &in.Web, &out.Web + *out = new(WebMetric) + (*in).DeepCopyInto(*out) + } + if in.Datadog != nil { + in, out := &in.Datadog, &out.Datadog + *out = new(DatadogMetric) + (*in).DeepCopyInto(*out) + } + if in.Wavefront != nil { + in, out := &in.Wavefront, &out.Wavefront + *out = new(WavefrontMetric) + **out = **in + } + if in.NewRelic != nil { + in, out := &in.NewRelic, &out.NewRelic + *out = new(NewRelicMetric) + (*in).DeepCopyInto(*out) + } + if in.Job != nil { + in, out := &in.Job, &out.Job + *out = new(JobMetric) + (*in).DeepCopyInto(*out) + } + if in.CloudWatch != nil { + in, out := &in.CloudWatch, &out.CloudWatch + *out = new(CloudWatchMetric) + (*in).DeepCopyInto(*out) + } + if in.Graphite != nil { + in, out := &in.Graphite, &out.Graphite + *out = new(GraphiteMetric) + **out = **in + } + if in.Influxdb != nil { + in, out := &in.Influxdb, &out.Influxdb + *out = new(InfluxdbMetric) + **out = **in + } + if in.SkyWalking != nil { + in, out := &in.SkyWalking, &out.SkyWalking + *out = new(SkyWalkingMetric) + **out = **in + } + if in.Plugin != nil { + in, out := &in.Plugin, &out.Plugin + *out = make(map[string]json.RawMessage, len(*in)) + for key, val := range *in { + var outVal []byte + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make(json.RawMessage, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricProvider. +func (in *MetricProvider) DeepCopy() *MetricProvider { + if in == nil { + return nil + } + out := new(MetricProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MetricResult) DeepCopyInto(out *MetricResult) { + *out = *in + if in.Measurements != nil { + in, out := &in.Measurements, &out.Measurements + *out = make([]Measurement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricResult. +func (in *MetricResult) DeepCopy() *MetricResult { + if in == nil { + return nil + } + out := new(MetricResult) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NewRelicMetric) DeepCopyInto(out *NewRelicMetric) { + *out = *in + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(int64) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NewRelicMetric. +func (in *NewRelicMetric) DeepCopy() *NewRelicMetric { + if in == nil { + return nil + } + out := new(NewRelicMetric) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NginxTrafficRouting) DeepCopyInto(out *NginxTrafficRouting) { + *out = *in + if in.AdditionalIngressAnnotations != nil { + in, out := &in.AdditionalIngressAnnotations, &out.AdditionalIngressAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.StableIngresses != nil { + in, out := &in.StableIngresses, &out.StableIngresses + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.CanaryIngressAnnotations != nil { + in, out := &in.CanaryIngressAnnotations, &out.CanaryIngressAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NginxTrafficRouting. +func (in *NginxTrafficRouting) DeepCopy() *NginxTrafficRouting { + if in == nil { + return nil + } + out := new(NginxTrafficRouting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OAuth2Config) DeepCopyInto(out *OAuth2Config) { + *out = *in + if in.Scopes != nil { + in, out := &in.Scopes, &out.Scopes + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OAuth2Config. +func (in *OAuth2Config) DeepCopy() *OAuth2Config { + if in == nil { + return nil + } + out := new(OAuth2Config) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObjectRef) DeepCopyInto(out *ObjectRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectRef. +func (in *ObjectRef) DeepCopy() *ObjectRef { + if in == nil { + return nil + } + out := new(ObjectRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PauseCondition) DeepCopyInto(out *PauseCondition) { + *out = *in + in.StartTime.DeepCopyInto(&out.StartTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PauseCondition. +func (in *PauseCondition) DeepCopy() *PauseCondition { + if in == nil { + return nil + } + out := new(PauseCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PingPongSpec) DeepCopyInto(out *PingPongSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PingPongSpec. +func (in *PingPongSpec) DeepCopy() *PingPongSpec { + if in == nil { + return nil + } + out := new(PingPongSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PluginStep) DeepCopyInto(out *PluginStep) { + *out = *in + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = make(json.RawMessage, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginStep. +func (in *PluginStep) DeepCopy() *PluginStep { + if in == nil { + return nil + } + out := new(PluginStep) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PodTemplateMetadata) DeepCopyInto(out *PodTemplateMetadata) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodTemplateMetadata. +func (in *PodTemplateMetadata) DeepCopy() *PodTemplateMetadata { + if in == nil { + return nil + } + out := new(PodTemplateMetadata) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PreferredDuringSchedulingIgnoredDuringExecution) DeepCopyInto(out *PreferredDuringSchedulingIgnoredDuringExecution) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PreferredDuringSchedulingIgnoredDuringExecution. +func (in *PreferredDuringSchedulingIgnoredDuringExecution) DeepCopy() *PreferredDuringSchedulingIgnoredDuringExecution { + if in == nil { + return nil + } + out := new(PreferredDuringSchedulingIgnoredDuringExecution) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PrometheusMetric) DeepCopyInto(out *PrometheusMetric) { + *out = *in + in.Authentication.DeepCopyInto(&out.Authentication) + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(int64) + **out = **in + } + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make([]WebMetricHeader, len(*in)) + copy(*out, *in) + } + if in.RangeQuery != nil { + in, out := &in.RangeQuery, &out.RangeQuery + *out = new(PrometheusRangeQueryArgs) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusMetric. +func (in *PrometheusMetric) DeepCopy() *PrometheusMetric { + if in == nil { + return nil + } + out := new(PrometheusMetric) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PrometheusRangeQueryArgs) DeepCopyInto(out *PrometheusRangeQueryArgs) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusRangeQueryArgs. +func (in *PrometheusRangeQueryArgs) DeepCopy() *PrometheusRangeQueryArgs { + if in == nil { + return nil + } + out := new(PrometheusRangeQueryArgs) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RequiredDuringSchedulingIgnoredDuringExecution) DeepCopyInto(out *RequiredDuringSchedulingIgnoredDuringExecution) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RequiredDuringSchedulingIgnoredDuringExecution. +func (in *RequiredDuringSchedulingIgnoredDuringExecution) DeepCopy() *RequiredDuringSchedulingIgnoredDuringExecution { + if in == nil { + return nil + } + out := new(RequiredDuringSchedulingIgnoredDuringExecution) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RollbackWindowSpec) DeepCopyInto(out *RollbackWindowSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RollbackWindowSpec. +func (in *RollbackWindowSpec) DeepCopy() *RollbackWindowSpec { + if in == nil { + return nil + } + out := new(RollbackWindowSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Rollout) DeepCopyInto(out *Rollout) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Rollout. +func (in *Rollout) DeepCopy() *Rollout { + if in == nil { + return nil + } + out := new(Rollout) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Rollout) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutAnalysis) DeepCopyInto(out *RolloutAnalysis) { + *out = *in + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = make([]AnalysisTemplateRef, len(*in)) + copy(*out, *in) + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]AnalysisRunArgument, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DryRun != nil { + in, out := &in.DryRun, &out.DryRun + *out = make([]DryRun, len(*in)) + copy(*out, *in) + } + if in.MeasurementRetention != nil { + in, out := &in.MeasurementRetention, &out.MeasurementRetention + *out = make([]MeasurementRetention, len(*in)) + copy(*out, *in) + } + in.AnalysisRunMetadata.DeepCopyInto(&out.AnalysisRunMetadata) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutAnalysis. +func (in *RolloutAnalysis) DeepCopy() *RolloutAnalysis { + if in == nil { + return nil + } + out := new(RolloutAnalysis) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutAnalysisBackground) DeepCopyInto(out *RolloutAnalysisBackground) { + *out = *in + in.RolloutAnalysis.DeepCopyInto(&out.RolloutAnalysis) + if in.StartingStep != nil { + in, out := &in.StartingStep, &out.StartingStep + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutAnalysisBackground. +func (in *RolloutAnalysisBackground) DeepCopy() *RolloutAnalysisBackground { + if in == nil { + return nil + } + out := new(RolloutAnalysisBackground) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutAnalysisRunStatus) DeepCopyInto(out *RolloutAnalysisRunStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutAnalysisRunStatus. +func (in *RolloutAnalysisRunStatus) DeepCopy() *RolloutAnalysisRunStatus { + if in == nil { + return nil + } + out := new(RolloutAnalysisRunStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutCondition) DeepCopyInto(out *RolloutCondition) { + *out = *in + in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutCondition. +func (in *RolloutCondition) DeepCopy() *RolloutCondition { + if in == nil { + return nil + } + out := new(RolloutCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutExperimentStep) DeepCopyInto(out *RolloutExperimentStep) { + *out = *in + if in.Templates != nil { + in, out := &in.Templates, &out.Templates + *out = make([]RolloutExperimentTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Analyses != nil { + in, out := &in.Analyses, &out.Analyses + *out = make([]RolloutExperimentStepAnalysisTemplateRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DryRun != nil { + in, out := &in.DryRun, &out.DryRun + *out = make([]DryRun, len(*in)) + copy(*out, *in) + } + in.AnalysisRunMetadata.DeepCopyInto(&out.AnalysisRunMetadata) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutExperimentStep. +func (in *RolloutExperimentStep) DeepCopy() *RolloutExperimentStep { + if in == nil { + return nil + } + out := new(RolloutExperimentStep) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutExperimentStepAnalysisTemplateRef) DeepCopyInto(out *RolloutExperimentStepAnalysisTemplateRef) { + *out = *in + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]AnalysisRunArgument, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutExperimentStepAnalysisTemplateRef. +func (in *RolloutExperimentStepAnalysisTemplateRef) DeepCopy() *RolloutExperimentStepAnalysisTemplateRef { + if in == nil { + return nil + } + out := new(RolloutExperimentStepAnalysisTemplateRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutExperimentTemplate) DeepCopyInto(out *RolloutExperimentTemplate) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + in.Metadata.DeepCopyInto(&out.Metadata) + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.Weight != nil { + in, out := &in.Weight, &out.Weight + *out = new(int32) + **out = **in + } + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(TemplateService) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutExperimentTemplate. +func (in *RolloutExperimentTemplate) DeepCopy() *RolloutExperimentTemplate { + if in == nil { + return nil + } + out := new(RolloutExperimentTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutList) DeepCopyInto(out *RolloutList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Rollout, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutList. +func (in *RolloutList) DeepCopy() *RolloutList { + if in == nil { + return nil + } + out := new(RolloutList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RolloutList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutPause) DeepCopyInto(out *RolloutPause) { + *out = *in + if in.Duration != nil { + in, out := &in.Duration, &out.Duration + *out = new(intstr.IntOrString) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutPause. +func (in *RolloutPause) DeepCopy() *RolloutPause { + if in == nil { + return nil + } + out := new(RolloutPause) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutSpec) DeepCopyInto(out *RolloutSpec) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + in.Template.DeepCopyInto(&out.Template) + if in.WorkloadRef != nil { + in, out := &in.WorkloadRef, &out.WorkloadRef + *out = new(ObjectRef) + **out = **in + } + if in.RollbackWindow != nil { + in, out := &in.RollbackWindow, &out.RollbackWindow + *out = new(RollbackWindowSpec) + **out = **in + } + in.Strategy.DeepCopyInto(&out.Strategy) + if in.RevisionHistoryLimit != nil { + in, out := &in.RevisionHistoryLimit, &out.RevisionHistoryLimit + *out = new(int32) + **out = **in + } + if in.ProgressDeadlineSeconds != nil { + in, out := &in.ProgressDeadlineSeconds, &out.ProgressDeadlineSeconds + *out = new(int32) + **out = **in + } + if in.RestartAt != nil { + in, out := &in.RestartAt, &out.RestartAt + *out = (*in).DeepCopy() + } + if in.Analysis != nil { + in, out := &in.Analysis, &out.Analysis + *out = new(AnalysisRunStrategy) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutSpec. +func (in *RolloutSpec) DeepCopy() *RolloutSpec { + if in == nil { + return nil + } + out := new(RolloutSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutStatus) DeepCopyInto(out *RolloutStatus) { + *out = *in + if in.PauseConditions != nil { + in, out := &in.PauseConditions, &out.PauseConditions + *out = make([]PauseCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AbortedAt != nil { + in, out := &in.AbortedAt, &out.AbortedAt + *out = (*in).DeepCopy() + } + if in.CurrentStepIndex != nil { + in, out := &in.CurrentStepIndex, &out.CurrentStepIndex + *out = new(int32) + **out = **in + } + if in.CollisionCount != nil { + in, out := &in.CollisionCount, &out.CollisionCount + *out = new(int32) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]RolloutCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.Canary.DeepCopyInto(&out.Canary) + in.BlueGreen.DeepCopyInto(&out.BlueGreen) + if in.RestartedAt != nil { + in, out := &in.RestartedAt, &out.RestartedAt + *out = (*in).DeepCopy() + } + if in.ALB != nil { + in, out := &in.ALB, &out.ALB + *out = new(ALBStatus) + **out = **in + } + if in.ALBs != nil { + in, out := &in.ALBs, &out.ALBs + *out = make([]ALBStatus, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutStatus. +func (in *RolloutStatus) DeepCopy() *RolloutStatus { + if in == nil { + return nil + } + out := new(RolloutStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutStrategy) DeepCopyInto(out *RolloutStrategy) { + *out = *in + if in.BlueGreen != nil { + in, out := &in.BlueGreen, &out.BlueGreen + *out = new(BlueGreenStrategy) + (*in).DeepCopyInto(*out) + } + if in.Canary != nil { + in, out := &in.Canary, &out.Canary + *out = new(CanaryStrategy) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutStrategy. +func (in *RolloutStrategy) DeepCopy() *RolloutStrategy { + if in == nil { + return nil + } + out := new(RolloutStrategy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RolloutTrafficRouting) DeepCopyInto(out *RolloutTrafficRouting) { + *out = *in + if in.Istio != nil { + in, out := &in.Istio, &out.Istio + *out = new(IstioTrafficRouting) + (*in).DeepCopyInto(*out) + } + if in.Nginx != nil { + in, out := &in.Nginx, &out.Nginx + *out = new(NginxTrafficRouting) + (*in).DeepCopyInto(*out) + } + if in.ALB != nil { + in, out := &in.ALB, &out.ALB + *out = new(ALBTrafficRouting) + (*in).DeepCopyInto(*out) + } + if in.SMI != nil { + in, out := &in.SMI, &out.SMI + *out = new(SMITrafficRouting) + **out = **in + } + if in.Ambassador != nil { + in, out := &in.Ambassador, &out.Ambassador + *out = new(AmbassadorTrafficRouting) + (*in).DeepCopyInto(*out) + } + if in.AppMesh != nil { + in, out := &in.AppMesh, &out.AppMesh + *out = new(AppMeshTrafficRouting) + (*in).DeepCopyInto(*out) + } + if in.Traefik != nil { + in, out := &in.Traefik, &out.Traefik + *out = new(TraefikTrafficRouting) + **out = **in + } + if in.ManagedRoutes != nil { + in, out := &in.ManagedRoutes, &out.ManagedRoutes + *out = make([]MangedRoutes, len(*in)) + copy(*out, *in) + } + if in.Apisix != nil { + in, out := &in.Apisix, &out.Apisix + *out = new(ApisixTrafficRouting) + (*in).DeepCopyInto(*out) + } + if in.Plugins != nil { + in, out := &in.Plugins, &out.Plugins + *out = make(map[string]json.RawMessage, len(*in)) + for key, val := range *in { + var outVal []byte + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make(json.RawMessage, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + if in.MaxTrafficWeight != nil { + in, out := &in.MaxTrafficWeight, &out.MaxTrafficWeight + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RolloutTrafficRouting. +func (in *RolloutTrafficRouting) DeepCopy() *RolloutTrafficRouting { + if in == nil { + return nil + } + out := new(RolloutTrafficRouting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RouteMatch) DeepCopyInto(out *RouteMatch) { + *out = *in + if in.Method != nil { + in, out := &in.Method, &out.Method + *out = new(StringMatch) + **out = **in + } + if in.Path != nil { + in, out := &in.Path, &out.Path + *out = new(StringMatch) + **out = **in + } + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make(map[string]StringMatch, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouteMatch. +func (in *RouteMatch) DeepCopy() *RouteMatch { + if in == nil { + return nil + } + out := new(RouteMatch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RunSummary) DeepCopyInto(out *RunSummary) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunSummary. +func (in *RunSummary) DeepCopy() *RunSummary { + if in == nil { + return nil + } + out := new(RunSummary) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SMITrafficRouting) DeepCopyInto(out *SMITrafficRouting) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SMITrafficRouting. +func (in *SMITrafficRouting) DeepCopy() *SMITrafficRouting { + if in == nil { + return nil + } + out := new(SMITrafficRouting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScopeDetail) DeepCopyInto(out *ScopeDetail) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeDetail. +func (in *ScopeDetail) DeepCopy() *ScopeDetail { + if in == nil { + return nil + } + out := new(ScopeDetail) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretKeyRef) DeepCopyInto(out *SecretKeyRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeyRef. +func (in *SecretKeyRef) DeepCopy() *SecretKeyRef { + if in == nil { + return nil + } + out := new(SecretKeyRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretRef) DeepCopyInto(out *SecretRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretRef. +func (in *SecretRef) DeepCopy() *SecretRef { + if in == nil { + return nil + } + out := new(SecretRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SetCanaryScale) DeepCopyInto(out *SetCanaryScale) { + *out = *in + if in.Weight != nil { + in, out := &in.Weight, &out.Weight + *out = new(int32) + **out = **in + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SetCanaryScale. +func (in *SetCanaryScale) DeepCopy() *SetCanaryScale { + if in == nil { + return nil + } + out := new(SetCanaryScale) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SetHeaderRoute) DeepCopyInto(out *SetHeaderRoute) { + *out = *in + if in.Match != nil { + in, out := &in.Match, &out.Match + *out = make([]HeaderRoutingMatch, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SetHeaderRoute. +func (in *SetHeaderRoute) DeepCopy() *SetHeaderRoute { + if in == nil { + return nil + } + out := new(SetHeaderRoute) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SetMirrorRoute) DeepCopyInto(out *SetMirrorRoute) { + *out = *in + if in.Match != nil { + in, out := &in.Match, &out.Match + *out = make([]RouteMatch, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Percentage != nil { + in, out := &in.Percentage, &out.Percentage + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SetMirrorRoute. +func (in *SetMirrorRoute) DeepCopy() *SetMirrorRoute { + if in == nil { + return nil + } + out := new(SetMirrorRoute) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Sigv4Config) DeepCopyInto(out *Sigv4Config) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Sigv4Config. +func (in *Sigv4Config) DeepCopy() *Sigv4Config { + if in == nil { + return nil + } + out := new(Sigv4Config) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SkyWalkingMetric) DeepCopyInto(out *SkyWalkingMetric) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SkyWalkingMetric. +func (in *SkyWalkingMetric) DeepCopy() *SkyWalkingMetric { + if in == nil { + return nil + } + out := new(SkyWalkingMetric) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StepPluginStatus) DeepCopyInto(out *StepPluginStatus) { + *out = *in + if in.StartedAt != nil { + in, out := &in.StartedAt, &out.StartedAt + *out = (*in).DeepCopy() + } + if in.UpdatedAt != nil { + in, out := &in.UpdatedAt, &out.UpdatedAt + *out = (*in).DeepCopy() + } + if in.FinishedAt != nil { + in, out := &in.FinishedAt, &out.FinishedAt + *out = (*in).DeepCopy() + } + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = make(json.RawMessage, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StepPluginStatus. +func (in *StepPluginStatus) DeepCopy() *StepPluginStatus { + if in == nil { + return nil + } + out := new(StepPluginStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StickinessConfig) DeepCopyInto(out *StickinessConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StickinessConfig. +func (in *StickinessConfig) DeepCopy() *StickinessConfig { + if in == nil { + return nil + } + out := new(StickinessConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StringMatch) DeepCopyInto(out *StringMatch) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StringMatch. +func (in *StringMatch) DeepCopy() *StringMatch { + if in == nil { + return nil + } + out := new(StringMatch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TCPRoute) DeepCopyInto(out *TCPRoute) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPRoute. +func (in *TCPRoute) DeepCopy() *TCPRoute { + if in == nil { + return nil + } + out := new(TCPRoute) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSRoute) DeepCopyInto(out *TLSRoute) { + *out = *in + if in.SNIHosts != nil { + in, out := &in.SNIHosts, &out.SNIHosts + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSRoute. +func (in *TLSRoute) DeepCopy() *TLSRoute { + if in == nil { + return nil + } + out := new(TLSRoute) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TTLStrategy) DeepCopyInto(out *TTLStrategy) { + *out = *in + if in.SecondsAfterCompletion != nil { + in, out := &in.SecondsAfterCompletion, &out.SecondsAfterCompletion + *out = new(int32) + **out = **in + } + if in.SecondsAfterFailure != nil { + in, out := &in.SecondsAfterFailure, &out.SecondsAfterFailure + *out = new(int32) + **out = **in + } + if in.SecondsAfterSuccess != nil { + in, out := &in.SecondsAfterSuccess, &out.SecondsAfterSuccess + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TTLStrategy. +func (in *TTLStrategy) DeepCopy() *TTLStrategy { + if in == nil { + return nil + } + out := new(TTLStrategy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplateService) DeepCopyInto(out *TemplateService) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateService. +func (in *TemplateService) DeepCopy() *TemplateService { + if in == nil { + return nil + } + out := new(TemplateService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplateSpec) DeepCopyInto(out *TemplateSpec) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + in.Template.DeepCopyInto(&out.Template) + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(TemplateService) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateSpec. +func (in *TemplateSpec) DeepCopy() *TemplateSpec { + if in == nil { + return nil + } + out := new(TemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TemplateStatus) DeepCopyInto(out *TemplateStatus) { + *out = *in + if in.CollisionCount != nil { + in, out := &in.CollisionCount, &out.CollisionCount + *out = new(int32) + **out = **in + } + if in.LastTransitionTime != nil { + in, out := &in.LastTransitionTime, &out.LastTransitionTime + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateStatus. +func (in *TemplateStatus) DeepCopy() *TemplateStatus { + if in == nil { + return nil + } + out := new(TemplateStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TraefikTrafficRouting) DeepCopyInto(out *TraefikTrafficRouting) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TraefikTrafficRouting. +func (in *TraefikTrafficRouting) DeepCopy() *TraefikTrafficRouting { + if in == nil { + return nil + } + out := new(TraefikTrafficRouting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrafficWeights) DeepCopyInto(out *TrafficWeights) { + *out = *in + out.Canary = in.Canary + out.Stable = in.Stable + if in.Additional != nil { + in, out := &in.Additional, &out.Additional + *out = make([]WeightDestination, len(*in)) + copy(*out, *in) + } + if in.Verified != nil { + in, out := &in.Verified, &out.Verified + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrafficWeights. +func (in *TrafficWeights) DeepCopy() *TrafficWeights { + if in == nil { + return nil + } + out := new(TrafficWeights) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValueFrom) DeepCopyInto(out *ValueFrom) { + *out = *in + if in.SecretKeyRef != nil { + in, out := &in.SecretKeyRef, &out.SecretKeyRef + *out = new(SecretKeyRef) + **out = **in + } + if in.FieldRef != nil { + in, out := &in.FieldRef, &out.FieldRef + *out = new(FieldRef) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValueFrom. +func (in *ValueFrom) DeepCopy() *ValueFrom { + if in == nil { + return nil + } + out := new(ValueFrom) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WavefrontMetric) DeepCopyInto(out *WavefrontMetric) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WavefrontMetric. +func (in *WavefrontMetric) DeepCopy() *WavefrontMetric { + if in == nil { + return nil + } + out := new(WavefrontMetric) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebMetric) DeepCopyInto(out *WebMetric) { + *out = *in + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make([]WebMetricHeader, len(*in)) + copy(*out, *in) + } + if in.JSONBody != nil { + in, out := &in.JSONBody, &out.JSONBody + *out = make(json.RawMessage, len(*in)) + copy(*out, *in) + } + in.Authentication.DeepCopyInto(&out.Authentication) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebMetric. +func (in *WebMetric) DeepCopy() *WebMetric { + if in == nil { + return nil + } + out := new(WebMetric) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebMetricHeader) DeepCopyInto(out *WebMetricHeader) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebMetricHeader. +func (in *WebMetricHeader) DeepCopy() *WebMetricHeader { + if in == nil { + return nil + } + out := new(WebMetricHeader) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WeightDestination) DeepCopyInto(out *WeightDestination) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WeightDestination. +func (in *WeightDestination) DeepCopy() *WeightDestination { + if in == nil { + return nil + } + out := new(WeightDestination) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/vendored/argo-rollouts/crds/analysis-run-crd.yaml b/pkg/vendored/argo-rollouts/crds/analysis-run-crd.yaml new file mode 100644 index 00000000..e6256b15 --- /dev/null +++ b/pkg/vendored/argo-rollouts/crds/analysis-run-crd.yaml @@ -0,0 +1,3319 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: analysisruns.argoproj.io +spec: + group: argoproj.io + names: + kind: AnalysisRun + listKind: AnalysisRunList + plural: analysisruns + shortNames: + - ar + singular: analysisrun + preserveUnknownFields: false + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: AnalysisRun status + jsonPath: .status.phase + name: Status + type: string + - description: Time since resource was created + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + metrics: + items: + properties: + consecutiveErrorLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + consecutiveSuccessLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + count: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + failureCondition: + type: string + failureLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + inconclusiveLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + initialDelay: + type: string + interval: + type: string + name: + type: string + provider: + properties: + cloudWatch: + properties: + interval: + type: string + metricDataQueries: + items: + properties: + expression: + type: string + id: + type: string + label: + type: string + metricStat: + properties: + metric: + properties: + dimensions: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + metricName: + type: string + namespace: + type: string + type: object + period: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + stat: + type: string + unit: + type: string + type: object + period: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + returnData: + type: boolean + type: object + type: array + required: + - metricDataQueries + type: object + datadog: + properties: + aggregator: + enum: + - avg + - min + - max + - sum + - last + - percentile + - mean + - l2norm + - area + type: string + apiVersion: + default: v1 + enum: + - v1 + - v2 + type: string + formula: + type: string + interval: + default: 5m + type: string + queries: + additionalProperties: + type: string + type: object + query: + type: string + secretRef: + properties: + name: + type: string + namespaced: + type: boolean + type: object + type: object + graphite: + properties: + address: + type: string + query: + type: string + type: object + influxdb: + properties: + profile: + type: string + query: + type: string + type: object + job: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + backoffLimit: + format: int32 + type: integer + backoffLimitPerIndex: + format: int32 + type: integer + completionMode: + type: string + completions: + format: int32 + type: integer + manualSelector: + type: boolean + maxFailedIndexes: + format: int32 + type: integer + parallelism: + format: int32 + type: integer + podFailurePolicy: + properties: + rules: + items: + properties: + action: + type: string + onExitCodes: + properties: + containerName: + type: string + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + items: + properties: + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + suspend: + type: boolean + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + searches: + items: + type: string + type: array + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + ip: + type: string + type: object + type: array + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + imagePullSecrets: + items: + properties: + name: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + initContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + resourceClaims: + items: + properties: + name: + type: string + source: + properties: + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + x-kubernetes-preserve-unknown-fields: true + required: + - containers + type: object + type: object + ttlSecondsAfterFinished: + format: int32 + type: integer + required: + - template + type: object + required: + - spec + type: object + kayenta: + properties: + address: + type: string + application: + type: string + canaryConfigName: + type: string + configurationAccountName: + type: string + metricsAccountName: + type: string + scopes: + items: + properties: + controlScope: + properties: + end: + type: string + region: + type: string + scope: + type: string + start: + type: string + step: + format: int64 + type: integer + required: + - end + - region + - scope + - start + - step + type: object + experimentScope: + properties: + end: + type: string + region: + type: string + scope: + type: string + start: + type: string + step: + format: int64 + type: integer + required: + - end + - region + - scope + - start + - step + type: object + name: + type: string + required: + - controlScope + - experimentScope + - name + type: object + type: array + storageAccountName: + type: string + threshold: + properties: + marginal: + format: int64 + type: integer + pass: + format: int64 + type: integer + required: + - marginal + - pass + type: object + required: + - address + - application + - canaryConfigName + - configurationAccountName + - metricsAccountName + - scopes + - storageAccountName + - threshold + type: object + newRelic: + properties: + profile: + type: string + query: + type: string + timeout: + format: int64 + type: integer + required: + - query + type: object + plugin: + type: object + x-kubernetes-preserve-unknown-fields: true + prometheus: + properties: + address: + type: string + authentication: + properties: + oauth2: + properties: + clientId: + type: string + clientSecret: + type: string + scopes: + items: + type: string + type: array + tokenUrl: + type: string + type: object + sigv4: + properties: + profile: + type: string + region: + type: string + roleArn: + type: string + type: object + type: object + headers: + items: + properties: + key: + type: string + value: + type: string + required: + - key + - value + type: object + type: array + insecure: + type: boolean + query: + type: string + rangeQuery: + properties: + end: + type: string + start: + type: string + step: + type: string + type: object + timeout: + format: int64 + type: integer + type: object + skywalking: + properties: + address: + type: string + interval: + type: string + query: + type: string + type: object + wavefront: + properties: + address: + type: string + query: + type: string + type: object + web: + properties: + authentication: + properties: + oauth2: + properties: + clientId: + type: string + clientSecret: + type: string + scopes: + items: + type: string + type: array + tokenUrl: + type: string + type: object + sigv4: + properties: + profile: + type: string + region: + type: string + roleArn: + type: string + type: object + type: object + body: + type: string + headers: + items: + properties: + key: + type: string + value: + type: string + required: + - key + - value + type: object + type: array + insecure: + type: boolean + jsonBody: + type: object + x-kubernetes-preserve-unknown-fields: true + jsonPath: + type: string + method: + type: string + timeoutSeconds: + format: int64 + type: integer + url: + type: string + required: + - url + type: object + type: object + successCondition: + type: string + required: + - name + - provider + type: object + type: array + terminate: + type: boolean + ttlStrategy: + properties: + secondsAfterCompletion: + format: int32 + type: integer + secondsAfterFailure: + format: int32 + type: integer + secondsAfterSuccess: + format: int32 + type: integer + type: object + required: + - metrics + type: object + status: + properties: + completedAt: + format: date-time + type: string + dryRunSummary: + properties: + count: + format: int32 + type: integer + error: + format: int32 + type: integer + failed: + format: int32 + type: integer + inconclusive: + format: int32 + type: integer + successful: + format: int32 + type: integer + type: object + message: + type: string + metricResults: + items: + properties: + consecutiveError: + format: int32 + type: integer + consecutiveSuccess: + format: int32 + type: integer + count: + format: int32 + type: integer + dryRun: + type: boolean + error: + format: int32 + type: integer + failed: + format: int32 + type: integer + inconclusive: + format: int32 + type: integer + measurements: + items: + properties: + finishedAt: + format: date-time + type: string + message: + type: string + metadata: + additionalProperties: + type: string + type: object + phase: + type: string + resumeAt: + format: date-time + type: string + startedAt: + format: date-time + type: string + value: + type: string + required: + - phase + type: object + type: array + message: + type: string + metadata: + additionalProperties: + type: string + type: object + name: + type: string + phase: + type: string + successful: + format: int32 + type: integer + required: + - name + - phase + type: object + type: array + phase: + type: string + runSummary: + properties: + count: + format: int32 + type: integer + error: + format: int32 + type: integer + failed: + format: int32 + type: integer + inconclusive: + format: int32 + type: integer + successful: + format: int32 + type: integer + type: object + startedAt: + format: date-time + type: string + required: + - phase + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} diff --git a/pkg/vendored/argo-rollouts/crds/analysis-template-crd.yaml b/pkg/vendored/argo-rollouts/crds/analysis-template-crd.yaml new file mode 100644 index 00000000..0b9f22d2 --- /dev/null +++ b/pkg/vendored/argo-rollouts/crds/analysis-template-crd.yaml @@ -0,0 +1,3190 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: analysistemplates.argoproj.io +spec: + group: argoproj.io + names: + kind: AnalysisTemplate + listKind: AnalysisTemplateList + plural: analysistemplates + shortNames: + - at + singular: analysistemplate + preserveUnknownFields: false + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Time since resource was created + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + metrics: + items: + properties: + consecutiveErrorLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + consecutiveSuccessLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + count: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + failureCondition: + type: string + failureLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + inconclusiveLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + initialDelay: + type: string + interval: + type: string + name: + type: string + provider: + properties: + cloudWatch: + properties: + interval: + type: string + metricDataQueries: + items: + properties: + expression: + type: string + id: + type: string + label: + type: string + metricStat: + properties: + metric: + properties: + dimensions: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + metricName: + type: string + namespace: + type: string + type: object + period: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + stat: + type: string + unit: + type: string + type: object + period: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + returnData: + type: boolean + type: object + type: array + required: + - metricDataQueries + type: object + datadog: + properties: + aggregator: + enum: + - avg + - min + - max + - sum + - last + - percentile + - mean + - l2norm + - area + type: string + apiVersion: + default: v1 + enum: + - v1 + - v2 + type: string + formula: + type: string + interval: + default: 5m + type: string + queries: + additionalProperties: + type: string + type: object + query: + type: string + secretRef: + properties: + name: + type: string + namespaced: + type: boolean + type: object + type: object + graphite: + properties: + address: + type: string + query: + type: string + type: object + influxdb: + properties: + profile: + type: string + query: + type: string + type: object + job: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + backoffLimit: + format: int32 + type: integer + backoffLimitPerIndex: + format: int32 + type: integer + completionMode: + type: string + completions: + format: int32 + type: integer + manualSelector: + type: boolean + maxFailedIndexes: + format: int32 + type: integer + parallelism: + format: int32 + type: integer + podFailurePolicy: + properties: + rules: + items: + properties: + action: + type: string + onExitCodes: + properties: + containerName: + type: string + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + items: + properties: + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + suspend: + type: boolean + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + searches: + items: + type: string + type: array + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + ip: + type: string + type: object + type: array + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + imagePullSecrets: + items: + properties: + name: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + initContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + resourceClaims: + items: + properties: + name: + type: string + source: + properties: + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + x-kubernetes-preserve-unknown-fields: true + required: + - containers + type: object + type: object + ttlSecondsAfterFinished: + format: int32 + type: integer + required: + - template + type: object + required: + - spec + type: object + kayenta: + properties: + address: + type: string + application: + type: string + canaryConfigName: + type: string + configurationAccountName: + type: string + metricsAccountName: + type: string + scopes: + items: + properties: + controlScope: + properties: + end: + type: string + region: + type: string + scope: + type: string + start: + type: string + step: + format: int64 + type: integer + required: + - end + - region + - scope + - start + - step + type: object + experimentScope: + properties: + end: + type: string + region: + type: string + scope: + type: string + start: + type: string + step: + format: int64 + type: integer + required: + - end + - region + - scope + - start + - step + type: object + name: + type: string + required: + - controlScope + - experimentScope + - name + type: object + type: array + storageAccountName: + type: string + threshold: + properties: + marginal: + format: int64 + type: integer + pass: + format: int64 + type: integer + required: + - marginal + - pass + type: object + required: + - address + - application + - canaryConfigName + - configurationAccountName + - metricsAccountName + - scopes + - storageAccountName + - threshold + type: object + newRelic: + properties: + profile: + type: string + query: + type: string + timeout: + format: int64 + type: integer + required: + - query + type: object + plugin: + type: object + x-kubernetes-preserve-unknown-fields: true + prometheus: + properties: + address: + type: string + authentication: + properties: + oauth2: + properties: + clientId: + type: string + clientSecret: + type: string + scopes: + items: + type: string + type: array + tokenUrl: + type: string + type: object + sigv4: + properties: + profile: + type: string + region: + type: string + roleArn: + type: string + type: object + type: object + headers: + items: + properties: + key: + type: string + value: + type: string + required: + - key + - value + type: object + type: array + insecure: + type: boolean + query: + type: string + rangeQuery: + properties: + end: + type: string + start: + type: string + step: + type: string + type: object + timeout: + format: int64 + type: integer + type: object + skywalking: + properties: + address: + type: string + interval: + type: string + query: + type: string + type: object + wavefront: + properties: + address: + type: string + query: + type: string + type: object + web: + properties: + authentication: + properties: + oauth2: + properties: + clientId: + type: string + clientSecret: + type: string + scopes: + items: + type: string + type: array + tokenUrl: + type: string + type: object + sigv4: + properties: + profile: + type: string + region: + type: string + roleArn: + type: string + type: object + type: object + body: + type: string + headers: + items: + properties: + key: + type: string + value: + type: string + required: + - key + - value + type: object + type: array + insecure: + type: boolean + jsonBody: + type: object + x-kubernetes-preserve-unknown-fields: true + jsonPath: + type: string + method: + type: string + timeoutSeconds: + format: int64 + type: integer + url: + type: string + required: + - url + type: object + type: object + successCondition: + type: string + required: + - name + - provider + type: object + type: array + templates: + items: + properties: + clusterScope: + type: boolean + templateName: + type: string + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} diff --git a/pkg/vendored/argo-rollouts/crds/cluster-analysis-template-crd.yaml b/pkg/vendored/argo-rollouts/crds/cluster-analysis-template-crd.yaml new file mode 100644 index 00000000..f966d4f8 --- /dev/null +++ b/pkg/vendored/argo-rollouts/crds/cluster-analysis-template-crd.yaml @@ -0,0 +1,3190 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: clusteranalysistemplates.argoproj.io +spec: + group: argoproj.io + names: + kind: ClusterAnalysisTemplate + listKind: ClusterAnalysisTemplateList + plural: clusteranalysistemplates + shortNames: + - cat + singular: clusteranalysistemplate + preserveUnknownFields: false + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Time since resource was created + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + metrics: + items: + properties: + consecutiveErrorLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + consecutiveSuccessLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + count: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + failureCondition: + type: string + failureLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + inconclusiveLimit: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + initialDelay: + type: string + interval: + type: string + name: + type: string + provider: + properties: + cloudWatch: + properties: + interval: + type: string + metricDataQueries: + items: + properties: + expression: + type: string + id: + type: string + label: + type: string + metricStat: + properties: + metric: + properties: + dimensions: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + metricName: + type: string + namespace: + type: string + type: object + period: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + stat: + type: string + unit: + type: string + type: object + period: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + returnData: + type: boolean + type: object + type: array + required: + - metricDataQueries + type: object + datadog: + properties: + aggregator: + enum: + - avg + - min + - max + - sum + - last + - percentile + - mean + - l2norm + - area + type: string + apiVersion: + default: v1 + enum: + - v1 + - v2 + type: string + formula: + type: string + interval: + default: 5m + type: string + queries: + additionalProperties: + type: string + type: object + query: + type: string + secretRef: + properties: + name: + type: string + namespaced: + type: boolean + type: object + type: object + graphite: + properties: + address: + type: string + query: + type: string + type: object + influxdb: + properties: + profile: + type: string + query: + type: string + type: object + job: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + backoffLimit: + format: int32 + type: integer + backoffLimitPerIndex: + format: int32 + type: integer + completionMode: + type: string + completions: + format: int32 + type: integer + manualSelector: + type: boolean + maxFailedIndexes: + format: int32 + type: integer + parallelism: + format: int32 + type: integer + podFailurePolicy: + properties: + rules: + items: + properties: + action: + type: string + onExitCodes: + properties: + containerName: + type: string + operator: + type: string + values: + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + items: + properties: + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + type: string + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + suspend: + type: boolean + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + searches: + items: + type: string + type: array + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + ip: + type: string + type: object + type: array + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + imagePullSecrets: + items: + properties: + name: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + initContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + resourceClaims: + items: + properties: + name: + type: string + source: + properties: + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + x-kubernetes-preserve-unknown-fields: true + required: + - containers + type: object + type: object + ttlSecondsAfterFinished: + format: int32 + type: integer + required: + - template + type: object + required: + - spec + type: object + kayenta: + properties: + address: + type: string + application: + type: string + canaryConfigName: + type: string + configurationAccountName: + type: string + metricsAccountName: + type: string + scopes: + items: + properties: + controlScope: + properties: + end: + type: string + region: + type: string + scope: + type: string + start: + type: string + step: + format: int64 + type: integer + required: + - end + - region + - scope + - start + - step + type: object + experimentScope: + properties: + end: + type: string + region: + type: string + scope: + type: string + start: + type: string + step: + format: int64 + type: integer + required: + - end + - region + - scope + - start + - step + type: object + name: + type: string + required: + - controlScope + - experimentScope + - name + type: object + type: array + storageAccountName: + type: string + threshold: + properties: + marginal: + format: int64 + type: integer + pass: + format: int64 + type: integer + required: + - marginal + - pass + type: object + required: + - address + - application + - canaryConfigName + - configurationAccountName + - metricsAccountName + - scopes + - storageAccountName + - threshold + type: object + newRelic: + properties: + profile: + type: string + query: + type: string + timeout: + format: int64 + type: integer + required: + - query + type: object + plugin: + type: object + x-kubernetes-preserve-unknown-fields: true + prometheus: + properties: + address: + type: string + authentication: + properties: + oauth2: + properties: + clientId: + type: string + clientSecret: + type: string + scopes: + items: + type: string + type: array + tokenUrl: + type: string + type: object + sigv4: + properties: + profile: + type: string + region: + type: string + roleArn: + type: string + type: object + type: object + headers: + items: + properties: + key: + type: string + value: + type: string + required: + - key + - value + type: object + type: array + insecure: + type: boolean + query: + type: string + rangeQuery: + properties: + end: + type: string + start: + type: string + step: + type: string + type: object + timeout: + format: int64 + type: integer + type: object + skywalking: + properties: + address: + type: string + interval: + type: string + query: + type: string + type: object + wavefront: + properties: + address: + type: string + query: + type: string + type: object + web: + properties: + authentication: + properties: + oauth2: + properties: + clientId: + type: string + clientSecret: + type: string + scopes: + items: + type: string + type: array + tokenUrl: + type: string + type: object + sigv4: + properties: + profile: + type: string + region: + type: string + roleArn: + type: string + type: object + type: object + body: + type: string + headers: + items: + properties: + key: + type: string + value: + type: string + required: + - key + - value + type: object + type: array + insecure: + type: boolean + jsonBody: + type: object + x-kubernetes-preserve-unknown-fields: true + jsonPath: + type: string + method: + type: string + timeoutSeconds: + format: int64 + type: integer + url: + type: string + required: + - url + type: object + type: object + successCondition: + type: string + required: + - name + - provider + type: object + type: array + templates: + items: + properties: + clusterScope: + type: boolean + templateName: + type: string + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} diff --git a/pkg/vendored/argo-rollouts/crds/experiment-crd.yaml b/pkg/vendored/argo-rollouts/crds/experiment-crd.yaml new file mode 100644 index 00000000..0dae59cb --- /dev/null +++ b/pkg/vendored/argo-rollouts/crds/experiment-crd.yaml @@ -0,0 +1,2858 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: experiments.argoproj.io +spec: + group: argoproj.io + names: + kind: Experiment + listKind: ExperimentList + plural: experiments + shortNames: + - exp + singular: experiment + preserveUnknownFields: false + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Experiment status + jsonPath: .status.phase + name: Status + type: string + - description: Time since resource was created + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + analyses: + items: + properties: + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + type: object + required: + - name + type: object + type: array + clusterScope: + type: boolean + name: + type: string + requiredForCompletion: + type: boolean + templateName: + type: string + required: + - name + - templateName + type: object + type: array + analysisRunMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + duration: + type: string + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + progressDeadlineSeconds: + format: int32 + type: integer + scaleDownDelaySeconds: + format: int32 + type: integer + templates: + items: + properties: + minReadySeconds: + format: int32 + type: integer + name: + type: string + replicas: + format: int32 + type: integer + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + service: + properties: + name: + type: string + type: object + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + searches: + items: + type: string + type: array + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + ip: + type: string + type: object + type: array + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + imagePullSecrets: + items: + properties: + name: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + initContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + resourceClaims: + items: + properties: + name: + type: string + source: + properties: + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + x-kubernetes-preserve-unknown-fields: true + required: + - containers + type: object + type: object + required: + - name + - selector + - template + type: object + type: array + terminate: + type: boolean + required: + - templates + type: object + status: + properties: + analysisRuns: + items: + properties: + analysisRun: + type: string + message: + type: string + name: + type: string + phase: + type: string + required: + - analysisRun + - name + - phase + type: object + type: array + availableAt: + format: date-time + type: string + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - lastTransitionTime + - lastUpdateTime + - message + - reason + - status + - type + type: object + type: array + message: + type: string + phase: + type: string + templateStatuses: + items: + properties: + availableReplicas: + format: int32 + type: integer + collisionCount: + format: int32 + type: integer + lastTransitionTime: + format: date-time + type: string + message: + type: string + name: + type: string + podTemplateHash: + type: string + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + serviceName: + type: string + status: + type: string + updatedReplicas: + format: int32 + type: integer + required: + - availableReplicas + - name + - readyReplicas + - replicas + - updatedReplicas + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} diff --git a/pkg/vendored/argo-rollouts/crds/rollout-crd.yaml b/pkg/vendored/argo-rollouts/crds/rollout-crd.yaml new file mode 100755 index 00000000..aa06634f --- /dev/null +++ b/pkg/vendored/argo-rollouts/crds/rollout-crd.yaml @@ -0,0 +1,3908 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: rollouts.argoproj.io +spec: + group: argoproj.io + names: + kind: Rollout + listKind: RolloutList + plural: rollouts + shortNames: + - ro + singular: rollout + preserveUnknownFields: false + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Number of desired pods + jsonPath: .spec.replicas + name: Desired + type: integer + - description: Total number of non-terminated pods targeted by this rollout + jsonPath: .status.replicas + name: Current + type: integer + - description: Total number of non-terminated pods targeted by this rollout that + have the desired template spec + jsonPath: .status.updatedReplicas + name: Up-to-date + type: integer + - description: Total number of available pods (ready for at least minReadySeconds) + targeted by this rollout + jsonPath: .status.availableReplicas + name: Available + type: integer + - description: Time since resource was created + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + analysis: + properties: + successfulRunHistoryLimit: + format: int32 + type: integer + unsuccessfulRunHistoryLimit: + format: int32 + type: integer + type: object + minReadySeconds: + format: int32 + type: integer + paused: + type: boolean + progressDeadlineAbort: + type: boolean + progressDeadlineSeconds: + format: int32 + type: integer + replicas: + format: int32 + type: integer + restartAt: + format: date-time + type: string + revisionHistoryLimit: + format: int32 + type: integer + rollbackWindow: + properties: + revisions: + format: int32 + type: integer + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + strategy: + properties: + blueGreen: + properties: + abortScaleDownDelaySeconds: + format: int32 + type: integer + activeMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + activeService: + type: string + antiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + properties: + weight: + format: int32 + type: integer + required: + - weight + type: object + requiredDuringSchedulingIgnoredDuringExecution: + type: object + type: object + autoPromotionEnabled: + type: boolean + autoPromotionSeconds: + format: int32 + type: integer + maxUnavailable: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + postPromotionAnalysis: + properties: + analysisRunMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + podTemplateHashValue: + type: string + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + templates: + items: + properties: + clusterScope: + type: boolean + templateName: + type: string + type: object + type: array + type: object + prePromotionAnalysis: + properties: + analysisRunMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + podTemplateHashValue: + type: string + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + templates: + items: + properties: + clusterScope: + type: boolean + templateName: + type: string + type: object + type: array + type: object + previewMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + previewReplicaCount: + format: int32 + type: integer + previewService: + type: string + scaleDownDelayRevisionLimit: + format: int32 + type: integer + scaleDownDelaySeconds: + format: int32 + type: integer + required: + - activeService + type: object + canary: + properties: + abortScaleDownDelaySeconds: + format: int32 + type: integer + analysis: + properties: + analysisRunMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + podTemplateHashValue: + type: string + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + startingStep: + format: int32 + type: integer + templates: + items: + properties: + clusterScope: + type: boolean + templateName: + type: string + type: object + type: array + type: object + antiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + properties: + weight: + format: int32 + type: integer + required: + - weight + type: object + requiredDuringSchedulingIgnoredDuringExecution: + type: object + type: object + canaryMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + canaryService: + type: string + dynamicStableScale: + type: boolean + maxSurge: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + minPodsPerReplicaSet: + format: int32 + type: integer + pingPong: + properties: + pingService: + type: string + pongService: + type: string + required: + - pingService + - pongService + type: object + scaleDownDelayRevisionLimit: + format: int32 + type: integer + scaleDownDelaySeconds: + format: int32 + type: integer + stableMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + stableService: + type: string + steps: + items: + properties: + analysis: + properties: + analysisRunMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + podTemplateHashValue: + type: string + type: object + required: + - name + type: object + type: array + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + measurementRetention: + items: + properties: + limit: + format: int32 + type: integer + metricName: + type: string + required: + - limit + - metricName + type: object + type: array + templates: + items: + properties: + clusterScope: + type: boolean + templateName: + type: string + type: object + type: array + type: object + experiment: + properties: + analyses: + items: + properties: + args: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + fieldRef: + properties: + fieldPath: + type: string + required: + - fieldPath + type: object + podTemplateHashValue: + type: string + type: object + required: + - name + type: object + type: array + clusterScope: + type: boolean + name: + type: string + requiredForCompletion: + type: boolean + templateName: + type: string + required: + - name + - templateName + type: object + type: array + analysisRunMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + dryRun: + items: + properties: + metricName: + type: string + required: + - metricName + type: object + type: array + duration: + type: string + templates: + items: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + name: + type: string + replicas: + format: int32 + type: integer + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + service: + properties: + name: + type: string + type: object + specRef: + type: string + weight: + format: int32 + type: integer + required: + - name + - specRef + type: object + type: array + required: + - templates + type: object + pause: + properties: + duration: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + plugin: + properties: + config: + type: object + x-kubernetes-preserve-unknown-fields: true + name: + type: string + required: + - name + type: object + setCanaryScale: + properties: + matchTrafficWeight: + type: boolean + replicas: + format: int32 + type: integer + weight: + format: int32 + type: integer + type: object + setHeaderRoute: + properties: + match: + items: + properties: + headerName: + type: string + headerValue: + properties: + exact: + type: string + prefix: + type: string + regex: + type: string + type: object + required: + - headerName + - headerValue + type: object + type: array + name: + type: string + type: object + setMirrorRoute: + properties: + match: + items: + properties: + headers: + additionalProperties: + properties: + exact: + type: string + prefix: + type: string + regex: + type: string + type: object + type: object + method: + properties: + exact: + type: string + prefix: + type: string + regex: + type: string + type: object + path: + properties: + exact: + type: string + prefix: + type: string + regex: + type: string + type: object + type: object + type: array + name: + type: string + percentage: + format: int32 + type: integer + required: + - name + type: object + setWeight: + format: int32 + type: integer + type: object + type: array + trafficRouting: + properties: + alb: + properties: + annotationPrefix: + type: string + ingress: + type: string + ingresses: + items: + type: string + type: array + rootService: + type: string + servicePort: + format: int32 + type: integer + stickinessConfig: + properties: + durationSeconds: + format: int64 + type: integer + enabled: + type: boolean + required: + - durationSeconds + - enabled + type: object + required: + - servicePort + type: object + ambassador: + properties: + mappings: + items: + type: string + type: array + required: + - mappings + type: object + apisix: + properties: + route: + properties: + name: + type: string + rules: + items: + type: string + type: array + required: + - name + type: object + type: object + appMesh: + properties: + virtualNodeGroup: + properties: + canaryVirtualNodeRef: + properties: + name: + type: string + required: + - name + type: object + stableVirtualNodeRef: + properties: + name: + type: string + required: + - name + type: object + required: + - canaryVirtualNodeRef + - stableVirtualNodeRef + type: object + virtualService: + properties: + name: + type: string + routes: + items: + type: string + type: array + required: + - name + type: object + type: object + istio: + properties: + destinationRule: + properties: + canarySubsetName: + type: string + name: + type: string + stableSubsetName: + type: string + required: + - canarySubsetName + - name + - stableSubsetName + type: object + virtualService: + properties: + name: + type: string + routes: + items: + type: string + type: array + tcpRoutes: + items: + properties: + port: + format: int64 + type: integer + type: object + type: array + tlsRoutes: + items: + properties: + port: + format: int64 + type: integer + sniHosts: + items: + type: string + type: array + type: object + type: array + required: + - name + type: object + virtualServices: + items: + properties: + name: + type: string + routes: + items: + type: string + type: array + tcpRoutes: + items: + properties: + port: + format: int64 + type: integer + type: object + type: array + tlsRoutes: + items: + properties: + port: + format: int64 + type: integer + sniHosts: + items: + type: string + type: array + type: object + type: array + required: + - name + type: object + type: array + type: object + managedRoutes: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + maxTrafficWeight: + format: int32 + type: integer + nginx: + properties: + additionalIngressAnnotations: + additionalProperties: + type: string + type: object + annotationPrefix: + type: string + canaryIngressAnnotations: + additionalProperties: + type: string + type: object + stableIngress: + type: string + stableIngresses: + items: + type: string + type: array + type: object + plugins: + type: object + x-kubernetes-preserve-unknown-fields: true + smi: + properties: + rootService: + type: string + trafficSplitName: + type: string + type: object + traefik: + properties: + weightedTraefikServiceName: + type: string + required: + - weightedTraefikServiceName + type: object + type: object + type: object + type: object + template: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + dnsConfig: + properties: + nameservers: + items: + type: string + type: array + options: + items: + properties: + name: + type: string + value: + type: string + type: object + type: array + searches: + items: + type: string + type: array + type: object + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + ip: + type: string + type: object + type: array + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + imagePullSecrets: + items: + properties: + name: + type: string + type: object + x-kubernetes-map-type: atomic + type: array + initContainers: + items: + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + type: string + lifecycle: + properties: + postStart: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + sleep: + properties: + seconds: + format: int64 + type: integer + required: + - seconds + type: object + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + default: TCP + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + properties: + resourceName: + type: string + restartPolicy: + type: string + required: + - resourceName + - restartPolicy + type: object + type: array + x-kubernetes-list-type: atomic + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + x-kubernetes-preserve-unknown-fields: true + requests: + x-kubernetes-preserve-unknown-fields: true + type: object + restartPolicy: + type: string + securityContext: + properties: + allowPrivilegeEscalation: + type: boolean + capabilities: + properties: + add: + items: + type: string + type: array + drop: + items: + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + properties: + exec: + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + properties: + port: + format: int32 + type: integer + service: + type: string + required: + - port + type: object + httpGet: + properties: + host: + type: string + httpHeaders: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + type: string + required: + - port + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + type: string + tty: + type: boolean + volumeDevices: + items: + properties: + devicePath: + type: string + name: + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + type: string + required: + - name + type: object + type: array + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + x-kubernetes-map-type: atomic + os: + properties: + name: + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + preemptionPolicy: + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + resourceClaims: + items: + properties: + name: + type: string + source: + properties: + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + restartPolicy: + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + properties: + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + topologySpreadConstraints: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + type: string + nodeTaintsPolicy: + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + items: + x-kubernetes-preserve-unknown-fields: true + type: array + required: + - containers + type: object + type: object + workloadRef: + properties: + apiVersion: + type: string + kind: + type: string + name: + type: string + scaleDown: + type: string + type: object + type: object + status: + properties: + HPAReplicas: + format: int32 + type: integer + abort: + type: boolean + abortedAt: + format: date-time + type: string + alb: + properties: + canaryTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + ingress: + type: string + loadBalancer: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + stableTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + type: object + albs: + items: + properties: + canaryTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + ingress: + type: string + loadBalancer: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + stableTargetGroup: + properties: + arn: + type: string + fullName: + type: string + name: + type: string + required: + - arn + - name + type: object + type: object + type: array + availableReplicas: + format: int32 + type: integer + blueGreen: + properties: + activeSelector: + type: string + postPromotionAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + prePromotionAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + previewSelector: + type: string + scaleUpPreviewCheckPoint: + type: boolean + type: object + canary: + properties: + currentBackgroundAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + currentExperiment: + type: string + currentStepAnalysisRunStatus: + properties: + message: + type: string + name: + type: string + status: + type: string + required: + - name + - status + type: object + stablePingPong: + type: string + stepPluginStatuses: + items: + properties: + backoff: + type: string + disabled: + type: boolean + executions: + format: int32 + type: integer + finishedAt: + format: date-time + type: string + index: + format: int32 + type: integer + message: + type: string + name: + type: string + operation: + type: string + phase: + type: string + startedAt: + format: date-time + type: string + status: + type: object + x-kubernetes-preserve-unknown-fields: true + updatedAt: + format: date-time + type: string + required: + - index + - name + - operation + type: object + type: array + weights: + properties: + additional: + items: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + type: array + canary: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + stable: + properties: + podTemplateHash: + type: string + serviceName: + type: string + weight: + format: int32 + type: integer + required: + - weight + type: object + verified: + type: boolean + required: + - canary + - stable + type: object + type: object + collisionCount: + format: int32 + type: integer + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - lastTransitionTime + - lastUpdateTime + - message + - reason + - status + - type + type: object + type: array + controllerPause: + type: boolean + currentPodHash: + type: string + currentStepHash: + type: string + currentStepIndex: + format: int32 + type: integer + message: + type: string + observedGeneration: + type: string + pauseConditions: + items: + properties: + reason: + type: string + startTime: + format: date-time + type: string + required: + - reason + - startTime + type: object + type: array + phase: + type: string + promoteFull: + type: boolean + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + restartedAt: + format: date-time + type: string + selector: + type: string + stableRS: + type: string + updatedReplicas: + format: int32 + type: integer + workloadObservedGeneration: + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.HPAReplicas + status: {} diff --git a/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_backendtlspolicies.yaml b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_backendtlspolicies.yaml new file mode 100644 index 00000000..58e4bf49 --- /dev/null +++ b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_backendtlspolicies.yaml @@ -0,0 +1,1382 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.0 + gateway.networking.k8s.io/channel: standard + labels: + gateway.networking.k8s.io/policy: Direct + name: backendtlspolicies.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: BackendTLSPolicy + listKind: BackendTLSPolicyList + plural: backendtlspolicies + shortNames: + - btlspolicy + singular: backendtlspolicy + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + BackendTLSPolicy provides a way to configure how a Gateway + connects to a Backend via TLS. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of BackendTLSPolicy. + properties: + options: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Options are a list of key/value pairs to enable extended TLS + configuration for each implementation. For example, configuring the + minimum TLS version or supported cipher suites. + + A set of common keys MAY be defined by the API in the future. To avoid + any ambiguity, implementation-specific definitions MUST use + domain-prefixed names, such as `example.com/my-custom-option`. + Un-prefixed names are reserved for key names defined by Gateway API. + + Support: Implementation-specific + maxProperties: 16 + type: object + targetRefs: + description: |- + TargetRefs identifies an API object to apply the policy to. + Note that this config applies to the entire referenced resource + by default, but this default may change in the future to provide + a more granular application of the policy. + + TargetRefs must be _distinct_. This means either that: + + * They select different targets. If this is the case, then targetRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, and `name` must + be unique across all targetRef entries in the BackendTLSPolicy. + * They select different sectionNames in the same target. + + When more than one BackendTLSPolicy selects the same target and + sectionName, implementations MUST determine precedence using the + following criteria, continuing on ties: + + * The older policy by creation timestamp takes precedence. For + example, a policy with a creation timestamp of "2021-07-15 + 01:02:03" MUST be given precedence over a policy with a + creation timestamp of "2021-07-15 01:02:04". + * The policy appearing first in alphabetical order by {namespace}/{name}. + For example, a policy named `foo/bar` is given precedence over a + policy named `foo/baz`. + + For any BackendTLSPolicy that does not take precedence, the + implementation MUST ensure the `Accepted` Condition is set to + `status: False`, with Reason `Conflicted`. + + Implementations SHOULD NOT support more than one targetRef at this + time. Although the API technically allows for this, the current guidance + for conflict resolution and status handling is lacking. Until that can be + clarified in a future release, the safest approach is to support a single + targetRef. + + Support Levels: + + * Extended: Kubernetes Service referenced by HTTPRoute backendRefs. + + * Implementation-Specific: Services not connected via HTTPRoute, and any + other kind of backend. Implementations MAY use BackendTLSPolicy for: + - Services not referenced by any Route (e.g., infrastructure services) + - Gateway feature backends (e.g., ExternalAuth, rate-limiting services) + - Service mesh workload-to-service communication + - Other resource types beyond Service + + Implementations SHOULD aim to ensure that BackendTLSPolicy behavior is consistent, + even outside of the extended HTTPRoute -(backendRef) -> Service path. + They SHOULD clearly document how BackendTLSPolicy is interpreted in these + scenarios, including: + - Which resources beyond Service are supported + - How the policy is discovered and applied + - Any implementation-specific semantics or restrictions + + Note that this config applies to the entire referenced resource + by default, but this default may change in the future to provide + a more granular application of the policy. + items: + description: |- + LocalPolicyTargetReferenceWithSectionName identifies an API object to apply a + direct policy to. This should be used as part of Policy resources that can + target single resources. For more information on how this policy attachment + mode works, and a sample Policy resource, refer to the policy attachment + documentation for Gateway API. + + Note: This should only be used for direct policy attachment when references + to SectionName are actually needed. In all other cases, + LocalPolicyTargetReference should be used. + properties: + group: + description: Group is the group of the target resource. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the target resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the target resource. + maxLength: 253 + minLength: 1 + type: string + sectionName: + description: |- + SectionName is the name of a section within the target resource. When + unspecified, this targetRef targets the entire resource. In the following + resources, SectionName is interpreted as the following: + + * Gateway: Listener name + * HTTPRoute: HTTPRouteRule name + * Service: Port name + + If a SectionName is specified, but does not exist on the targeted object, + the Policy must fail to attach, and the policy implementation should record + a `ResolvedRefs` or similar Condition in the Policy's status. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when targetRefs includes + 2 or more references to the same target + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name ? ((!has(p1.sectionName) || p1.sectionName + == '''') == (!has(p2.sectionName) || p2.sectionName == '''')) + : true))' + - message: sectionName must be unique when targetRefs includes 2 or + more references to the same target + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.sectionName) || + p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + validation: + description: Validation contains backend TLS validation configuration. + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes objects that + contain a PEM-encoded TLS CA certificate bundle, which is used to + validate a TLS handshake between the Gateway and backend Pod. + + If CACertificateRefs is empty or unspecified, then WellKnownCACertificates must be + specified. Only one of CACertificateRefs or WellKnownCACertificates may be specified, + not both. If CACertificateRefs is empty or unspecified, the configuration for + WellKnownCACertificates MUST be honored instead if supported by the implementation. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the referenced resource + does not exist) or is misconfigured (e.g., a ConfigMap does not contain a key + named `ca.crt`). In this case, the Reason must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this case, the Reason + must be set to `InvalidKind` and the Message of the Condition must explain which + kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace. This may change in future + spec updates. + + Implementations MAY choose to perform further validation of the certificate + content (e.g., checking expiry or enforcing specific formats). In such cases, + an implementation-specific Reason and Message must be set for the invalid reference. + + In all cases, the implementation MUST ensure the `ResolvedRefs` Condition on + the BackendTLSPolicy is set to `status: False`, with a Reason and Message + that indicate the cause of the error. Connections using an invalid + CACertificateRef MUST fail, and the client MUST receive an HTTP 5xx error + response. If ALL CACertificateRefs are invalid, the implementation MUST also + ensure the `Accepted` Condition on the BackendTLSPolicy is set to + `status: False`, with a Reason `NoValidCACertificate`. + + A single CACertificateRef to a Kubernetes ConfigMap kind has "Core" support. + Implementations MAY choose to support attaching multiple certificates to + a backend, but this behavior is implementation-specific. + + Support: Core - An optional single reference to a Kubernetes ConfigMap, + with the CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + LocalObjectReference identifies an API object within the namespace of the + referrer. + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example "HTTPRoute" + or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + hostname: + description: |- + Hostname is used for two purposes in the connection between Gateways and + backends: + + 1. Hostname MUST be used as the SNI to connect to the backend (RFC 6066). + 2. Hostname MUST be used for authentication and MUST match the certificate + served by the matching backend, unless SubjectAltNames is specified. + 3. If SubjectAltNames are specified, Hostname can be used for certificate selection + but MUST NOT be used for authentication. If you want to use the value + of the Hostname field for authentication, you MUST add it to the SubjectAltNames list. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + subjectAltNames: + description: |- + SubjectAltNames contains one or more Subject Alternative Names. + When specified the certificate served from the backend MUST + have at least one Subject Alternate Name matching one of the specified SubjectAltNames. + + Support: Extended + items: + description: SubjectAltName represents Subject Alternative Name. + properties: + hostname: + description: |- + Hostname contains Subject Alternative Name specified in DNS name format. + Required when Type is set to Hostname, ignored otherwise. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: + description: |- + Type determines the format of the Subject Alternative Name. Always required. + + Support: Core + enum: + - Hostname + - URI + type: string + uri: + description: |- + URI contains Subject Alternative Name specified in a full URI format. + It MUST include both a scheme (e.g., "http" or "ftp") and a scheme-specific-part. + Common values include SPIFFE IDs like "spiffe://mycluster.example.com/ns/myns/sa/svc1sa". + Required when Type is set to URI, ignored otherwise. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(([^:/?#]+):)(//([^/?#]*))([^?#]*)(\?([^#]*))?(#(.*))? + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: SubjectAltName element must contain Hostname, if + Type is set to Hostname + rule: '!(self.type == "Hostname" && (!has(self.hostname) || + self.hostname == ""))' + - message: SubjectAltName element must not contain Hostname, + if Type is not set to Hostname + rule: '!(self.type != "Hostname" && has(self.hostname) && + self.hostname != "")' + - message: SubjectAltName element must contain URI, if Type + is set to URI + rule: '!(self.type == "URI" && (!has(self.uri) || self.uri + == ""))' + - message: SubjectAltName element must not contain URI, if Type + is not set to URI + rule: '!(self.type != "URI" && has(self.uri) && self.uri != + "")' + maxItems: 5 + type: array + x-kubernetes-list-type: atomic + wellKnownCACertificates: + description: |- + WellKnownCACertificates specifies whether a well-known set of CA certificates + may be used in the TLS handshake between the gateway and backend pod. + + If WellKnownCACertificates is unspecified or empty (""), then CACertificateRefs + must be specified with at least one entry for a valid configuration. Only one of + CACertificateRefs or WellKnownCACertificates may be specified, not both. + If an implementation does not support the WellKnownCACertificates field, or + the supplied value is not recognized, the implementation MUST ensure the + `Accepted` Condition on the BackendTLSPolicy is set to `status: False`, with + a Reason `Invalid`. + + Valid values include: + * "System" - indicates that well-known system CA certificates should be used. + + Implementations MAY define their own sets of CA certificates. Such definitions + MUST use an implementation-specific, prefixed name, such as + `mycompany.com/my-custom-ca-certificates`. + + Support: Implementation-specific + maxLength: 253 + minLength: 1 + pattern: ^(System|([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_.]{0,61})?[A-Za-z0-9]))$ + type: string + required: + - hostname + type: object + x-kubernetes-validations: + - message: must not contain both CACertificateRefs and WellKnownCACertificates + rule: '!(has(self.caCertificateRefs) && size(self.caCertificateRefs) + > 0 && has(self.wellKnownCACertificates) && self.wellKnownCACertificates + != "")' + - message: must specify either CACertificateRefs or WellKnownCACertificates + rule: (has(self.caCertificateRefs) && size(self.caCertificateRefs) + > 0 || has(self.wellKnownCACertificates) && self.wellKnownCACertificates + != "") + required: + - targetRefs + - validation + type: object + status: + description: Status defines the current state of BackendTLSPolicy. + properties: + ancestors: + description: |- + Ancestors is a list of ancestor resources (usually Gateways) that are + associated with the policy, and the status of the policy with respect to + each ancestor. When this policy attaches to a parent, the controller that + manages the parent and the ancestors MUST add an entry to this list when + the controller first sees the policy and SHOULD update the entry as + appropriate when the relevant ancestor is modified. + + Note that choosing the relevant ancestor is left to the Policy designers; + an important part of Policy design is designing the right object level at + which to namespace this status. + + Note also that implementations MUST ONLY populate ancestor status for + the Ancestor resources they are responsible for. Implementations MUST + use the ControllerName field to uniquely identify the entries in this list + that they are responsible for. + + Note that to achieve this, the list of PolicyAncestorStatus structs + MUST be treated as a map with a composite key, made up of the AncestorRef + and ControllerName fields combined. + + A maximum of 16 ancestors will be represented in this list. An empty list + means the Policy is not relevant for any ancestors. + + If this slice is full, implementations MUST NOT add further entries. + Instead they MUST consider the policy unimplementable and signal that + on any related resources such as the ancestor that would be referenced + here. For example, if this list was full on BackendTLSPolicy, no + additional Gateways would be able to reference the Service targeted by + the BackendTLSPolicy. + items: + description: |- + PolicyAncestorStatus describes the status of a route with respect to an + associated Ancestor. + + Ancestors refer to objects that are either the Target of a policy or above it + in terms of object hierarchy. For example, if a policy targets a Service, the + Policy's Ancestors are, in order, the Service, the HTTPRoute, the Gateway, and + the GatewayClass. Almost always, in this hierarchy, the Gateway will be the most + useful object to place Policy status on, so we recommend that implementations + SHOULD use Gateway as the PolicyAncestorStatus object unless the designers + have a _very_ good reason otherwise. + + In the context of policy attachment, the Ancestor is used to distinguish which + resource results in a distinct application of this policy. For example, if a policy + targets a Service, it may have a distinct result per attached Gateway. + + Policies targeting the same resource may have different effects depending on the + ancestors of those resources. For example, different Gateways targeting the same + Service may have different capabilities, especially if they have different underlying + implementations. + + For example, in BackendTLSPolicy, the Policy attaches to a Service that is + used as a backend in a HTTPRoute that is itself attached to a Gateway. + In this case, the relevant object for status is the Gateway, and that is the + ancestor object referred to in this status. + + Note that a parent is also an ancestor, so for objects where the parent is the + relevant object for status, this struct SHOULD still be used. + + This struct is intended to be used in a slice that's effectively a map, + with a composite key made up of the AncestorRef and the ControllerName. + properties: + ancestorRef: + description: |- + AncestorRef corresponds with a ParentRef in the spec that this + PolicyAncestorStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + conditions: + description: Conditions describes the status of the Policy with + respect to the given Ancestor. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + required: + - ancestorRef + - conditions + - controllerName + type: object + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + required: + - ancestors + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - deprecated: true + deprecationWarning: The v1alpha3 version of BackendTLSPolicy has been deprecated + and will be removed in a future release of the API. Please upgrade to v1. + name: v1alpha3 + schema: + openAPIV3Schema: + description: |- + BackendTLSPolicy provides a way to configure how a Gateway + connects to a Backend via TLS. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of BackendTLSPolicy. + properties: + options: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Options are a list of key/value pairs to enable extended TLS + configuration for each implementation. For example, configuring the + minimum TLS version or supported cipher suites. + + A set of common keys MAY be defined by the API in the future. To avoid + any ambiguity, implementation-specific definitions MUST use + domain-prefixed names, such as `example.com/my-custom-option`. + Un-prefixed names are reserved for key names defined by Gateway API. + + Support: Implementation-specific + maxProperties: 16 + type: object + targetRefs: + description: |- + TargetRefs identifies an API object to apply the policy to. + Note that this config applies to the entire referenced resource + by default, but this default may change in the future to provide + a more granular application of the policy. + + TargetRefs must be _distinct_. This means either that: + + * They select different targets. If this is the case, then targetRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, and `name` must + be unique across all targetRef entries in the BackendTLSPolicy. + * They select different sectionNames in the same target. + + When more than one BackendTLSPolicy selects the same target and + sectionName, implementations MUST determine precedence using the + following criteria, continuing on ties: + + * The older policy by creation timestamp takes precedence. For + example, a policy with a creation timestamp of "2021-07-15 + 01:02:03" MUST be given precedence over a policy with a + creation timestamp of "2021-07-15 01:02:04". + * The policy appearing first in alphabetical order by {namespace}/{name}. + For example, a policy named `foo/bar` is given precedence over a + policy named `foo/baz`. + + For any BackendTLSPolicy that does not take precedence, the + implementation MUST ensure the `Accepted` Condition is set to + `status: False`, with Reason `Conflicted`. + + Implementations SHOULD NOT support more than one targetRef at this + time. Although the API technically allows for this, the current guidance + for conflict resolution and status handling is lacking. Until that can be + clarified in a future release, the safest approach is to support a single + targetRef. + + Support Levels: + + * Extended: Kubernetes Service referenced by HTTPRoute backendRefs. + + * Implementation-Specific: Services not connected via HTTPRoute, and any + other kind of backend. Implementations MAY use BackendTLSPolicy for: + - Services not referenced by any Route (e.g., infrastructure services) + - Gateway feature backends (e.g., ExternalAuth, rate-limiting services) + - Service mesh workload-to-service communication + - Other resource types beyond Service + + Implementations SHOULD aim to ensure that BackendTLSPolicy behavior is consistent, + even outside of the extended HTTPRoute -(backendRef) -> Service path. + They SHOULD clearly document how BackendTLSPolicy is interpreted in these + scenarios, including: + - Which resources beyond Service are supported + - How the policy is discovered and applied + - Any implementation-specific semantics or restrictions + + Note that this config applies to the entire referenced resource + by default, but this default may change in the future to provide + a more granular application of the policy. + items: + description: |- + LocalPolicyTargetReferenceWithSectionName identifies an API object to apply a + direct policy to. This should be used as part of Policy resources that can + target single resources. For more information on how this policy attachment + mode works, and a sample Policy resource, refer to the policy attachment + documentation for Gateway API. + + Note: This should only be used for direct policy attachment when references + to SectionName are actually needed. In all other cases, + LocalPolicyTargetReference should be used. + properties: + group: + description: Group is the group of the target resource. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the target resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the target resource. + maxLength: 253 + minLength: 1 + type: string + sectionName: + description: |- + SectionName is the name of a section within the target resource. When + unspecified, this targetRef targets the entire resource. In the following + resources, SectionName is interpreted as the following: + + * Gateway: Listener name + * HTTPRoute: HTTPRouteRule name + * Service: Port name + + If a SectionName is specified, but does not exist on the targeted object, + the Policy must fail to attach, and the policy implementation should record + a `ResolvedRefs` or similar Condition in the Policy's status. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when targetRefs includes + 2 or more references to the same target + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name ? ((!has(p1.sectionName) || p1.sectionName + == '''') == (!has(p2.sectionName) || p2.sectionName == '''')) + : true))' + - message: sectionName must be unique when targetRefs includes 2 or + more references to the same target + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.sectionName) || + p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + validation: + description: Validation contains backend TLS validation configuration. + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes objects that + contain a PEM-encoded TLS CA certificate bundle, which is used to + validate a TLS handshake between the Gateway and backend Pod. + + If CACertificateRefs is empty or unspecified, then WellKnownCACertificates must be + specified. Only one of CACertificateRefs or WellKnownCACertificates may be specified, + not both. If CACertificateRefs is empty or unspecified, the configuration for + WellKnownCACertificates MUST be honored instead if supported by the implementation. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the referenced resource + does not exist) or is misconfigured (e.g., a ConfigMap does not contain a key + named `ca.crt`). In this case, the Reason must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this case, the Reason + must be set to `InvalidKind` and the Message of the Condition must explain which + kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace. This may change in future + spec updates. + + Implementations MAY choose to perform further validation of the certificate + content (e.g., checking expiry or enforcing specific formats). In such cases, + an implementation-specific Reason and Message must be set for the invalid reference. + + In all cases, the implementation MUST ensure the `ResolvedRefs` Condition on + the BackendTLSPolicy is set to `status: False`, with a Reason and Message + that indicate the cause of the error. Connections using an invalid + CACertificateRef MUST fail, and the client MUST receive an HTTP 5xx error + response. If ALL CACertificateRefs are invalid, the implementation MUST also + ensure the `Accepted` Condition on the BackendTLSPolicy is set to + `status: False`, with a Reason `NoValidCACertificate`. + + A single CACertificateRef to a Kubernetes ConfigMap kind has "Core" support. + Implementations MAY choose to support attaching multiple certificates to + a backend, but this behavior is implementation-specific. + + Support: Core - An optional single reference to a Kubernetes ConfigMap, + with the CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + LocalObjectReference identifies an API object within the namespace of the + referrer. + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example "HTTPRoute" + or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + hostname: + description: |- + Hostname is used for two purposes in the connection between Gateways and + backends: + + 1. Hostname MUST be used as the SNI to connect to the backend (RFC 6066). + 2. Hostname MUST be used for authentication and MUST match the certificate + served by the matching backend, unless SubjectAltNames is specified. + 3. If SubjectAltNames are specified, Hostname can be used for certificate selection + but MUST NOT be used for authentication. If you want to use the value + of the Hostname field for authentication, you MUST add it to the SubjectAltNames list. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + subjectAltNames: + description: |- + SubjectAltNames contains one or more Subject Alternative Names. + When specified the certificate served from the backend MUST + have at least one Subject Alternate Name matching one of the specified SubjectAltNames. + + Support: Extended + items: + description: SubjectAltName represents Subject Alternative Name. + properties: + hostname: + description: |- + Hostname contains Subject Alternative Name specified in DNS name format. + Required when Type is set to Hostname, ignored otherwise. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: + description: |- + Type determines the format of the Subject Alternative Name. Always required. + + Support: Core + enum: + - Hostname + - URI + type: string + uri: + description: |- + URI contains Subject Alternative Name specified in a full URI format. + It MUST include both a scheme (e.g., "http" or "ftp") and a scheme-specific-part. + Common values include SPIFFE IDs like "spiffe://mycluster.example.com/ns/myns/sa/svc1sa". + Required when Type is set to URI, ignored otherwise. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(([^:/?#]+):)(//([^/?#]*))([^?#]*)(\?([^#]*))?(#(.*))? + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: SubjectAltName element must contain Hostname, if + Type is set to Hostname + rule: '!(self.type == "Hostname" && (!has(self.hostname) || + self.hostname == ""))' + - message: SubjectAltName element must not contain Hostname, + if Type is not set to Hostname + rule: '!(self.type != "Hostname" && has(self.hostname) && + self.hostname != "")' + - message: SubjectAltName element must contain URI, if Type + is set to URI + rule: '!(self.type == "URI" && (!has(self.uri) || self.uri + == ""))' + - message: SubjectAltName element must not contain URI, if Type + is not set to URI + rule: '!(self.type != "URI" && has(self.uri) && self.uri != + "")' + maxItems: 5 + type: array + x-kubernetes-list-type: atomic + wellKnownCACertificates: + description: |- + WellKnownCACertificates specifies whether a well-known set of CA certificates + may be used in the TLS handshake between the gateway and backend pod. + + If WellKnownCACertificates is unspecified or empty (""), then CACertificateRefs + must be specified with at least one entry for a valid configuration. Only one of + CACertificateRefs or WellKnownCACertificates may be specified, not both. + If an implementation does not support the WellKnownCACertificates field, or + the supplied value is not recognized, the implementation MUST ensure the + `Accepted` Condition on the BackendTLSPolicy is set to `status: False`, with + a Reason `Invalid`. + + Valid values include: + * "System" - indicates that well-known system CA certificates should be used. + + Implementations MAY define their own sets of CA certificates. Such definitions + MUST use an implementation-specific, prefixed name, such as + `mycompany.com/my-custom-ca-certificates`. + + Support: Implementation-specific + maxLength: 253 + minLength: 1 + pattern: ^(System|([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_.]{0,61})?[A-Za-z0-9]))$ + type: string + required: + - hostname + type: object + x-kubernetes-validations: + - message: must not contain both CACertificateRefs and WellKnownCACertificates + rule: '!(has(self.caCertificateRefs) && size(self.caCertificateRefs) + > 0 && has(self.wellKnownCACertificates) && self.wellKnownCACertificates + != "")' + - message: must specify either CACertificateRefs or WellKnownCACertificates + rule: (has(self.caCertificateRefs) && size(self.caCertificateRefs) + > 0 || has(self.wellKnownCACertificates) && self.wellKnownCACertificates + != "") + required: + - targetRefs + - validation + type: object + status: + description: Status defines the current state of BackendTLSPolicy. + properties: + ancestors: + description: |- + Ancestors is a list of ancestor resources (usually Gateways) that are + associated with the policy, and the status of the policy with respect to + each ancestor. When this policy attaches to a parent, the controller that + manages the parent and the ancestors MUST add an entry to this list when + the controller first sees the policy and SHOULD update the entry as + appropriate when the relevant ancestor is modified. + + Note that choosing the relevant ancestor is left to the Policy designers; + an important part of Policy design is designing the right object level at + which to namespace this status. + + Note also that implementations MUST ONLY populate ancestor status for + the Ancestor resources they are responsible for. Implementations MUST + use the ControllerName field to uniquely identify the entries in this list + that they are responsible for. + + Note that to achieve this, the list of PolicyAncestorStatus structs + MUST be treated as a map with a composite key, made up of the AncestorRef + and ControllerName fields combined. + + A maximum of 16 ancestors will be represented in this list. An empty list + means the Policy is not relevant for any ancestors. + + If this slice is full, implementations MUST NOT add further entries. + Instead they MUST consider the policy unimplementable and signal that + on any related resources such as the ancestor that would be referenced + here. For example, if this list was full on BackendTLSPolicy, no + additional Gateways would be able to reference the Service targeted by + the BackendTLSPolicy. + items: + description: |- + PolicyAncestorStatus describes the status of a route with respect to an + associated Ancestor. + + Ancestors refer to objects that are either the Target of a policy or above it + in terms of object hierarchy. For example, if a policy targets a Service, the + Policy's Ancestors are, in order, the Service, the HTTPRoute, the Gateway, and + the GatewayClass. Almost always, in this hierarchy, the Gateway will be the most + useful object to place Policy status on, so we recommend that implementations + SHOULD use Gateway as the PolicyAncestorStatus object unless the designers + have a _very_ good reason otherwise. + + In the context of policy attachment, the Ancestor is used to distinguish which + resource results in a distinct application of this policy. For example, if a policy + targets a Service, it may have a distinct result per attached Gateway. + + Policies targeting the same resource may have different effects depending on the + ancestors of those resources. For example, different Gateways targeting the same + Service may have different capabilities, especially if they have different underlying + implementations. + + For example, in BackendTLSPolicy, the Policy attaches to a Service that is + used as a backend in a HTTPRoute that is itself attached to a Gateway. + In this case, the relevant object for status is the Gateway, and that is the + ancestor object referred to in this status. + + Note that a parent is also an ancestor, so for objects where the parent is the + relevant object for status, this struct SHOULD still be used. + + This struct is intended to be used in a slice that's effectively a map, + with a composite key made up of the AncestorRef and the ControllerName. + properties: + ancestorRef: + description: |- + AncestorRef corresponds with a ParentRef in the spec that this + PolicyAncestorStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + conditions: + description: Conditions describes the status of the Policy with + respect to the given Ancestor. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + required: + - ancestorRef + - conditions + - controllerName + type: object + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + required: + - ancestors + type: object + required: + - spec + type: object + served: false + storage: false + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_gatewayclasses.yaml b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_gatewayclasses.yaml new file mode 100644 index 00000000..15412b86 --- /dev/null +++ b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_gatewayclasses.yaml @@ -0,0 +1,515 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.0 + gateway.networking.k8s.io/channel: standard + name: gatewayclasses.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: GatewayClass + listKind: GatewayClassList + plural: gatewayclasses + shortNames: + - gc + singular: gatewayclass + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.controllerName + name: Controller + type: string + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .spec.description + name: Description + priority: 1 + type: string + name: v1 + schema: + openAPIV3Schema: + description: |- + GatewayClass describes a class of Gateways available to the user for creating + Gateway resources. + + It is recommended that this resource be used as a template for Gateways. This + means that a Gateway is based on the state of the GatewayClass at the time it + was created and changes to the GatewayClass or associated parameters are not + propagated down to existing Gateways. This recommendation is intended to + limit the blast radius of changes to GatewayClass or associated parameters. + If implementations choose to propagate GatewayClass changes to existing + Gateways, that MUST be clearly documented by the implementation. + + Whenever one or more Gateways are using a GatewayClass, implementations SHOULD + add the `gateway-exists-finalizer.gateway.networking.k8s.io` finalizer on the + associated GatewayClass. This ensures that a GatewayClass associated with a + Gateway is not deleted while in use. + + GatewayClass is a Cluster level resource. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of GatewayClass. + properties: + controllerName: + description: |- + ControllerName is the name of the controller that is managing Gateways of + this class. The value of this field MUST be a domain prefixed path. + + Example: "example.net/gateway-controller". + + This field is not mutable and cannot be empty. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + description: + description: Description helps describe a GatewayClass with more details. + maxLength: 64 + type: string + parametersRef: + description: |- + ParametersRef is a reference to a resource that contains the configuration + parameters corresponding to the GatewayClass. This is optional if the + controller does not require any additional configuration. + + ParametersRef can reference a standard Kubernetes resource, i.e. ConfigMap, + or an implementation-specific custom resource. The resource can be + cluster-scoped or namespace-scoped. + + If the referent cannot be found, refers to an unsupported kind, or when + the data within that resource is malformed, the GatewayClass SHOULD be + rejected with the "Accepted" status condition set to "False" and an + "InvalidParameters" reason. + + A Gateway for this GatewayClass may provide its own `parametersRef`. When both are specified, + the merging behavior is implementation specific. + It is generally recommended that GatewayClass provides defaults that can be overridden by a Gateway. + + Support: Implementation-specific + properties: + group: + description: Group is the group of the referent. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. + This field is required when referring to a Namespace-scoped resource and + MUST be unset when referring to a Cluster-scoped resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + required: + - controllerName + type: object + status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + description: |- + Status defines the current state of GatewayClass. + + Implementations MUST populate status on all GatewayClass resources which + specify their controller name. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + description: |- + Conditions is the current status from the controller for + this GatewayClass. + + Controllers should prefer to publish conditions using values + of GatewayClassConditionType for the type of each Condition. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + supportedFeatures: + description: |- + SupportedFeatures is the set of features the GatewayClass support. + It MUST be sorted in ascending alphabetical order by the Name key. + items: + properties: + name: + description: |- + FeatureName is used to describe distinct features that are covered by + conformance tests. + type: string + required: + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.controllerName + name: Controller + type: string + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .spec.description + name: Description + priority: 1 + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + GatewayClass describes a class of Gateways available to the user for creating + Gateway resources. + + It is recommended that this resource be used as a template for Gateways. This + means that a Gateway is based on the state of the GatewayClass at the time it + was created and changes to the GatewayClass or associated parameters are not + propagated down to existing Gateways. This recommendation is intended to + limit the blast radius of changes to GatewayClass or associated parameters. + If implementations choose to propagate GatewayClass changes to existing + Gateways, that MUST be clearly documented by the implementation. + + Whenever one or more Gateways are using a GatewayClass, implementations SHOULD + add the `gateway-exists-finalizer.gateway.networking.k8s.io` finalizer on the + associated GatewayClass. This ensures that a GatewayClass associated with a + Gateway is not deleted while in use. + + GatewayClass is a Cluster level resource. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of GatewayClass. + properties: + controllerName: + description: |- + ControllerName is the name of the controller that is managing Gateways of + this class. The value of this field MUST be a domain prefixed path. + + Example: "example.net/gateway-controller". + + This field is not mutable and cannot be empty. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + description: + description: Description helps describe a GatewayClass with more details. + maxLength: 64 + type: string + parametersRef: + description: |- + ParametersRef is a reference to a resource that contains the configuration + parameters corresponding to the GatewayClass. This is optional if the + controller does not require any additional configuration. + + ParametersRef can reference a standard Kubernetes resource, i.e. ConfigMap, + or an implementation-specific custom resource. The resource can be + cluster-scoped or namespace-scoped. + + If the referent cannot be found, refers to an unsupported kind, or when + the data within that resource is malformed, the GatewayClass SHOULD be + rejected with the "Accepted" status condition set to "False" and an + "InvalidParameters" reason. + + A Gateway for this GatewayClass may provide its own `parametersRef`. When both are specified, + the merging behavior is implementation specific. + It is generally recommended that GatewayClass provides defaults that can be overridden by a Gateway. + + Support: Implementation-specific + properties: + group: + description: Group is the group of the referent. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. + This field is required when referring to a Namespace-scoped resource and + MUST be unset when referring to a Cluster-scoped resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + required: + - controllerName + type: object + status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + description: |- + Status defines the current state of GatewayClass. + + Implementations MUST populate status on all GatewayClass resources which + specify their controller name. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + description: |- + Conditions is the current status from the controller for + this GatewayClass. + + Controllers should prefer to publish conditions using values + of GatewayClassConditionType for the type of each Condition. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + supportedFeatures: + description: |- + SupportedFeatures is the set of features the GatewayClass support. + It MUST be sorted in ascending alphabetical order by the Name key. + items: + properties: + name: + description: |- + FeatureName is used to describe distinct features that are covered by + conformance tests. + type: string + required: + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_gateways.yaml b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_gateways.yaml new file mode 100644 index 00000000..169e74fc --- /dev/null +++ b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_gateways.yaml @@ -0,0 +1,3283 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.0 + gateway.networking.k8s.io/channel: standard + name: gateways.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: Gateway + listKind: GatewayList + plural: gateways + shortNames: + - gtw + singular: gateway + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.gatewayClassName + name: Class + type: string + - jsonPath: .status.addresses[*].value + name: Address + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + Gateway represents an instance of a service-traffic handling infrastructure + by binding Listeners to a set of IP addresses. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of Gateway. + properties: + addresses: + description: |- + Addresses requested for this Gateway. This is optional and behavior can + depend on the implementation. If a value is set in the spec and the + requested address is invalid or unavailable, the implementation MUST + indicate this in an associated entry in GatewayStatus.Conditions. + + The Addresses field represents a request for the address(es) on the + "outside of the Gateway", that traffic bound for this Gateway will use. + This could be the IP address or hostname of an external load balancer or + other networking infrastructure, or some other address that traffic will + be sent to. + + If no Addresses are specified, the implementation MAY schedule the + Gateway in an implementation-specific manner, assigning an appropriate + set of Addresses. + + The implementation MUST bind all Listeners to every GatewayAddress that + it assigns to the Gateway and add a corresponding entry in + GatewayStatus.Addresses. + + Support: Extended + items: + description: GatewaySpecAddress describes an address that can be + bound to a Gateway. + oneOf: + - properties: + type: + enum: + - IPAddress + value: + anyOf: + - format: ipv4 + - format: ipv6 + - properties: + type: + not: + enum: + - IPAddress + properties: + type: + default: IPAddress + description: Type of the address. + maxLength: 253 + minLength: 1 + pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + value: + description: |- + When a value is unspecified, an implementation SHOULD automatically + assign an address matching the requested type if possible. + + If an implementation does not support an empty value, they MUST set the + "Programmed" condition in status to False with a reason of "AddressNotAssigned". + + Examples: `1.2.3.4`, `128::1`, `my-ip-address`. + maxLength: 253 + type: string + type: object + x-kubernetes-validations: + - message: Hostname value must be empty or contain only valid characters + (matching ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$) + rule: 'self.type == ''Hostname'' ? (!has(self.value) || self.value.matches(r"""^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$""")): + true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: IPAddress values must be unique + rule: 'self.all(a1, a1.type == ''IPAddress'' && has(a1.value) ? + self.exists_one(a2, a2.type == a1.type && has(a2.value) && a2.value + == a1.value) : true )' + - message: Hostname values must be unique + rule: 'self.all(a1, a1.type == ''Hostname'' && has(a1.value) ? + self.exists_one(a2, a2.type == a1.type && has(a2.value) && a2.value + == a1.value) : true )' + allowedListeners: + description: |- + AllowedListeners defines which ListenerSets can be attached to this Gateway. + The default value is to allow no ListenerSets. + properties: + namespaces: + default: + from: None + description: |- + Namespaces defines which namespaces ListenerSets can be attached to this Gateway. + The default value is to allow no ListenerSets. + properties: + from: + default: None + description: |- + From indicates where ListenerSets can attach to this Gateway. Possible + values are: + + * Same: Only ListenerSets in the same namespace may be attached to this Gateway. + * Selector: ListenerSets in namespaces selected by the selector may be attached to this Gateway. + * All: ListenerSets in all namespaces may be attached to this Gateway. + * None: Only listeners defined in the Gateway's spec are allowed + + The default value None + enum: + - All + - Selector + - Same + - None + type: string + selector: + description: |- + Selector must be specified when From is set to "Selector". In that case, + only ListenerSets in Namespaces matching this Selector will be selected by this + Gateway. This field is ignored for other values of "From". + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: object + gatewayClassName: + description: |- + GatewayClassName used for this Gateway. This is the name of a + GatewayClass resource. + maxLength: 253 + minLength: 1 + type: string + infrastructure: + description: |- + Infrastructure defines infrastructure level attributes about this Gateway instance. + + Support: Extended + properties: + annotations: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Annotations that SHOULD be applied to any resources created in response to this Gateway. + + For implementations creating other Kubernetes objects, this should be the `metadata.annotations` field on resources. + For other implementations, this refers to any relevant (implementation specific) "annotations" concepts. + + An implementation may chose to add additional implementation-specific annotations as they see fit. + + Support: Extended + maxProperties: 8 + type: object + x-kubernetes-validations: + - message: Annotation keys must be in the form of an optional + DNS subdomain prefix followed by a required name segment of + up to 63 characters. + rule: self.all(key, key.matches(r"""^([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_.]{0,61})?[A-Za-z0-9]$""")) + - message: If specified, the annotation key's prefix must be a + DNS subdomain not longer than 253 characters in total. + rule: self.all(key, key.split("/")[0].size() < 253) + labels: + additionalProperties: + description: |- + LabelValue is the value of a label in the Gateway API. This is used for validation + of maps such as Gateway infrastructure labels. This matches the Kubernetes + label validation rules: + * must be 63 characters or less (can be empty), + * unless empty, must begin and end with an alphanumeric character ([a-z0-9A-Z]), + * could contain dashes (-), underscores (_), dots (.), and alphanumerics between. + + Valid values include: + + * MyValue + * my.name + * 123-my-value + maxLength: 63 + minLength: 0 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + description: |- + Labels that SHOULD be applied to any resources created in response to this Gateway. + + For implementations creating other Kubernetes objects, this should be the `metadata.labels` field on resources. + For other implementations, this refers to any relevant (implementation specific) "labels" concepts. + + An implementation may chose to add additional implementation-specific labels as they see fit. + + If an implementation maps these labels to Pods, or any other resource that would need to be recreated when labels + change, it SHOULD clearly warn about this behavior in documentation. + + Support: Extended + maxProperties: 8 + type: object + x-kubernetes-validations: + - message: Label keys must be in the form of an optional DNS subdomain + prefix followed by a required name segment of up to 63 characters. + rule: self.all(key, key.matches(r"""^([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_.]{0,61})?[A-Za-z0-9]$""")) + - message: If specified, the label key's prefix must be a DNS + subdomain not longer than 253 characters in total. + rule: self.all(key, key.split("/")[0].size() < 253) + parametersRef: + description: |- + ParametersRef is a reference to a resource that contains the configuration + parameters corresponding to the Gateway. This is optional if the + controller does not require any additional configuration. + + This follows the same semantics as GatewayClass's `parametersRef`, but on a per-Gateway basis + + The Gateway's GatewayClass may provide its own `parametersRef`. When both are specified, + the merging behavior is implementation specific. + It is generally recommended that GatewayClass provides defaults that can be overridden by a Gateway. + + If the referent cannot be found, refers to an unsupported kind, or when + the data within that resource is malformed, the Gateway SHOULD be + rejected with the "Accepted" status condition set to "False" and an + "InvalidParameters" reason. + + Support: Implementation-specific + properties: + group: + description: Group is the group of the referent. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + type: object + listeners: + description: |- + Listeners associated with this Gateway. Listeners define + logical endpoints that are bound on this Gateway's addresses. + At least one Listener MUST be specified. + + ## Distinct Listeners + + Each Listener in a set of Listeners (for example, in a single Gateway) + MUST be _distinct_, in that a traffic flow MUST be able to be assigned to + exactly one listener. (This section uses "set of Listeners" rather than + "Listeners in a single Gateway" because implementations MAY merge configuration + from multiple Gateways onto a single data plane, and these rules _also_ + apply in that case). + + Practically, this means that each listener in a set MUST have a unique + combination of Port, Protocol, and, if supported by the protocol, Hostname. + + Some combinations of port, protocol, and TLS settings are considered + Core support and MUST be supported by implementations based on the objects + they support: + + HTTPRoute + + 1. HTTPRoute, Port: 80, Protocol: HTTP + 2. HTTPRoute, Port: 443, Protocol: HTTPS, TLS Mode: Terminate, TLS keypair provided + + TLSRoute + + 1. TLSRoute, Port: 443, Protocol: TLS, TLS Mode: Passthrough + + "Distinct" Listeners have the following property: + + **The implementation can match inbound requests to a single distinct + Listener**. + + When multiple Listeners share values for fields (for + example, two Listeners with the same Port value), the implementation + can match requests to only one of the Listeners using other + Listener fields. + + When multiple listeners have the same value for the Protocol field, then + each of the Listeners with matching Protocol values MUST have different + values for other fields. + + The set of fields that MUST be different for a Listener differs per protocol. + The following rules define the rules for what fields MUST be considered for + Listeners to be distinct with each protocol currently defined in the + Gateway API spec. + + The set of listeners that all share a protocol value MUST have _different_ + values for _at least one_ of these fields to be distinct: + + * **HTTP, HTTPS, TLS**: Port, Hostname + * **TCP, UDP**: Port + + One **very** important rule to call out involves what happens when an + implementation: + + * Supports TCP protocol Listeners, as well as HTTP, HTTPS, or TLS protocol + Listeners, and + * sees HTTP, HTTPS, or TLS protocols with the same `port` as one with TCP + Protocol. + + In this case all the Listeners that share a port with the + TCP Listener are not distinct and so MUST NOT be accepted. + + If an implementation does not support TCP Protocol Listeners, then the + previous rule does not apply, and the TCP Listeners SHOULD NOT be + accepted. + + Note that the `tls` field is not used for determining if a listener is distinct, because + Listeners that _only_ differ on TLS config will still conflict in all cases. + + ### Listeners that are distinct only by Hostname + + When the Listeners are distinct based only on Hostname, inbound request + hostnames MUST match from the most specific to least specific Hostname + values to choose the correct Listener and its associated set of Routes. + + Exact matches MUST be processed before wildcard matches, and wildcard + matches MUST be processed before fallback (empty Hostname value) + matches. For example, `"foo.example.com"` takes precedence over + `"*.example.com"`, and `"*.example.com"` takes precedence over `""`. + + Additionally, if there are multiple wildcard entries, more specific + wildcard entries must be processed before less specific wildcard entries. + For example, `"*.foo.example.com"` takes precedence over `"*.example.com"`. + + The precise definition here is that the higher the number of dots in the + hostname to the right of the wildcard character, the higher the precedence. + + The wildcard character will match any number of characters _and dots_ to + the left, however, so `"*.example.com"` will match both + `"foo.bar.example.com"` _and_ `"bar.example.com"`. + + ## Handling indistinct Listeners + + If a set of Listeners contains Listeners that are not distinct, then those + Listeners are _Conflicted_, and the implementation MUST set the "Conflicted" + condition in the Listener Status to "True". + + The words "indistinct" and "conflicted" are considered equivalent for the + purpose of this documentation. + + Implementations MAY choose to accept a Gateway with some Conflicted + Listeners only if they only accept the partial Listener set that contains + no Conflicted Listeners. + + Specifically, an implementation MAY accept a partial Listener set subject to + the following rules: + + * The implementation MUST NOT pick one conflicting Listener as the winner. + ALL indistinct Listeners must not be accepted for processing. + * At least one distinct Listener MUST be present, or else the Gateway effectively + contains _no_ Listeners, and must be rejected from processing as a whole. + + The implementation MUST set a "ListenersNotValid" condition on the + Gateway Status when the Gateway contains Conflicted Listeners whether or + not they accept the Gateway. That Condition SHOULD clearly + indicate in the Message which Listeners are conflicted, and which are + Accepted. Additionally, the Listener status for those listeners SHOULD + indicate which Listeners are conflicted and not Accepted. + + ## General Listener behavior + + Note that, for all distinct Listeners, requests SHOULD match at most one Listener. + For example, if Listeners are defined for "foo.example.com" and "*.example.com", a + request to "foo.example.com" SHOULD only be routed using routes attached + to the "foo.example.com" Listener (and not the "*.example.com" Listener). + + This concept is known as "Listener Isolation", and it is an Extended feature + of Gateway API. Implementations that do not support Listener Isolation MUST + clearly document this, and MUST NOT claim support for the + `GatewayHTTPListenerIsolation` feature. + + Implementations that _do_ support Listener Isolation SHOULD claim support + for the Extended `GatewayHTTPListenerIsolation` feature and pass the associated + conformance tests. + + ## Compatible Listeners + + A Gateway's Listeners are considered _compatible_ if: + + 1. They are distinct. + 2. The implementation can serve them in compliance with the Addresses + requirement that all Listeners are available on all assigned + addresses. + + Compatible combinations in Extended support are expected to vary across + implementations. A combination that is compatible for one implementation + may not be compatible for another. + + For example, an implementation that cannot serve both TCP and UDP listeners + on the same address, or cannot mix HTTPS and generic TLS listens on the same port + would not consider those cases compatible, even though they are distinct. + + Implementations MAY merge separate Gateways onto a single set of + Addresses if all Listeners across all Gateways are compatible. + + In a future release the MinItems=1 requirement MAY be dropped. + + Support: Core + items: + description: |- + Listener embodies the concept of a logical endpoint where a Gateway accepts + network connections. + properties: + allowedRoutes: + default: + namespaces: + from: Same + description: |- + AllowedRoutes defines the types of routes that MAY be attached to a + Listener and the trusted namespaces where those Route resources MAY be + present. + + Although a client request may match multiple route rules, only one rule + may ultimately receive the request. Matching precedence MUST be + determined in order of the following criteria: + + * The most specific match as defined by the Route type. + * The oldest Route based on creation timestamp. For example, a Route with + a creation timestamp of "2020-09-08 01:02:03" is given precedence over + a Route with a creation timestamp of "2020-09-08 01:02:04". + * If everything else is equivalent, the Route appearing first in + alphabetical order (namespace/name) should be given precedence. For + example, foo/bar is given precedence over foo/baz. + + All valid rules within a Route attached to this Listener should be + implemented. Invalid Route rules can be ignored (sometimes that will mean + the full Route). If a Route rule transitions from valid to invalid, + support for that Route rule should be dropped to ensure consistency. For + example, even if a filter specified by a Route rule is invalid, the rest + of the rules within that Route should still be supported. + + Support: Core + properties: + kinds: + description: |- + Kinds specifies the groups and kinds of Routes that are allowed to bind + to this Gateway Listener. When unspecified or empty, the kinds of Routes + selected are determined using the Listener protocol. + + A RouteGroupKind MUST correspond to kinds of Routes that are compatible + with the application protocol specified in the Listener's Protocol field. + If an implementation does not support or recognize this resource type, it + MUST set the "ResolvedRefs" condition to False for this Listener with the + "InvalidRouteKinds" reason. + + Support: Core + items: + description: RouteGroupKind indicates the group and kind + of a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + namespaces: + default: + from: Same + description: |- + Namespaces indicates namespaces from which Routes may be attached to this + Listener. This is restricted to the namespace of this Gateway by default. + + Support: Core + properties: + from: + default: Same + description: |- + From indicates where Routes will be selected for this Gateway. Possible + values are: + + * All: Routes in all namespaces may be used by this Gateway. + * Selector: Routes in namespaces selected by the selector may be used by + this Gateway. + * Same: Only Routes in the same namespace may be used by this Gateway. + + Support: Core + enum: + - All + - Selector + - Same + type: string + selector: + description: |- + Selector must be specified when From is set to "Selector". In that case, + only Routes in Namespaces matching this Selector will be selected by this + Gateway. This field is ignored for other values of "From". + + Support: Core + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: object + hostname: + description: |- + Hostname specifies the virtual hostname to match for protocol types that + define this concept. When unspecified, all hostnames are matched. This + field is ignored for protocols that don't require hostname based + matching. + + Implementations MUST apply Hostname matching appropriately for each of + the following protocols: + + * TLS: The Listener Hostname MUST match the SNI. + * HTTP: The Listener Hostname MUST match the Host header of the request. + * HTTPS: The Listener Hostname SHOULD match both the SNI and Host header. + Note that this does not require the SNI and Host header to be the same. + The semantics of this are described in more detail below. + + To ensure security, Section 11.1 of RFC-6066 emphasizes that server + implementations that rely on SNI hostname matching MUST also verify + hostnames within the application protocol. + + Section 9.1.2 of RFC-7540 provides a mechanism for servers to reject the + reuse of a connection by responding with the HTTP 421 Misdirected Request + status code. This indicates that the origin server has rejected the + request because it appears to have been misdirected. + + To detect misdirected requests, Gateways SHOULD match the authority of + the requests with all the SNI hostname(s) configured across all the + Gateway Listeners on the same port and protocol: + + * If another Listener has an exact match or more specific wildcard entry, + the Gateway SHOULD return a 421. + * If the current Listener (selected by SNI matching during ClientHello) + does not match the Host: + * If another Listener does match the Host, the Gateway SHOULD return a + 421. + * If no other Listener matches the Host, the Gateway MUST return a + 404. + + For HTTPRoute and TLSRoute resources, there is an interaction with the + `spec.hostnames` array. When both listener and route specify hostnames, + there MUST be an intersection between the values for a Route to be + accepted. For more information, refer to the Route specific Hostnames + documentation. + + Hostnames that are prefixed with a wildcard label (`*.`) are interpreted + as a suffix match. That means that a match for `*.example.com` would match + both `test.example.com`, and `foo.test.example.com`, but not `example.com`. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + name: + description: |- + Name is the name of the Listener. This name MUST be unique within a + Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + port: + description: |- + Port is the network port. Multiple listeners may use the + same port, subject to the Listener compatibility rules. + + Support: Core + format: int32 + maximum: 65535 + minimum: 1 + type: integer + protocol: + description: |- + Protocol specifies the network protocol this listener expects to receive. + + Support: Core + maxLength: 255 + minLength: 1 + pattern: ^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?$|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9]+$ + type: string + tls: + description: |- + TLS is the TLS configuration for the Listener. This field is required if + the Protocol field is "HTTPS" or "TLS". It is invalid to set this field + if the Protocol field is "HTTP", "TCP", or "UDP". + + The association of SNIs to Certificate defined in ListenerTLSConfig is + defined based on the Hostname field for this listener. + + The GatewayClass MUST use the longest matching SNI out of all + available certificates for any TLS handshake. + + Support: Core + properties: + certificateRefs: + description: |- + CertificateRefs contains a series of references to Kubernetes objects that + contains TLS certificates and private keys. These certificates are used to + establish a TLS handshake for requests that match the hostname of the + associated listener. + + A single CertificateRef to a Kubernetes Secret has "Core" support. + Implementations MAY choose to support attaching multiple certificates to + a Listener, but this behavior is implementation-specific. + + References to a resource in different namespace are invalid UNLESS there + is a ReferenceGrant in the target namespace that allows the certificate + to be attached. If a ReferenceGrant does not allow this reference, the + "ResolvedRefs" condition MUST be set to False for this listener with the + "RefNotPermitted" reason. + + This field is required to have at least one element when the mode is set + to "Terminate" (default) and is optional otherwise. + + CertificateRefs can reference to standard Kubernetes resources, i.e. + Secret, or implementation-specific custom resources. + + Support: Core - A single reference to a Kubernetes Secret of type kubernetes.io/tls + + Support: Implementation-specific (More than one reference or other resource types) + items: + description: |- + SecretObjectReference identifies an API object including its namespace, + defaulting to Secret. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For example + "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + mode: + default: Terminate + description: |- + Mode defines the TLS behavior for the TLS session initiated by the client. + There are two possible modes: + + - Terminate: The TLS session between the downstream client and the + Gateway is terminated at the Gateway. This mode requires certificates + to be specified in some way, such as populating the certificateRefs + field. + - Passthrough: The TLS session is NOT terminated by the Gateway. This + implies that the Gateway can't decipher the TLS stream except for + the ClientHello message of the TLS protocol. The certificateRefs field + is ignored in this mode. + + Support: Core + enum: + - Terminate + - Passthrough + type: string + options: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Options are a list of key/value pairs to enable extended TLS + configuration for each implementation. For example, configuring the + minimum TLS version or supported cipher suites. + + A set of common keys MAY be defined by the API in the future. To avoid + any ambiguity, implementation-specific definitions MUST use + domain-prefixed names, such as `example.com/my-custom-option`. + Un-prefixed names are reserved for key names defined by Gateway API. + + Support: Implementation-specific + maxProperties: 16 + type: object + type: object + x-kubernetes-validations: + - message: certificateRefs or options must be specified when + mode is Terminate + rule: 'self.mode == ''Terminate'' ? size(self.certificateRefs) + > 0 || size(self.options) > 0 : true' + required: + - name + - port + - protocol + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: tls must not be specified for protocols ['HTTP', 'TCP', + 'UDP'] + rule: 'self.all(l, l.protocol in [''HTTP'', ''TCP'', ''UDP''] ? + !has(l.tls) : true)' + - message: tls mode must be Terminate for protocol HTTPS + rule: 'self.all(l, (l.protocol == ''HTTPS'' && has(l.tls)) ? (l.tls.mode + == '''' || l.tls.mode == ''Terminate'') : true)' + - message: tls mode must be set for protocol TLS + rule: 'self.all(l, (l.protocol == ''TLS'' ? has(l.tls) && has(l.tls.mode) + && l.tls.mode != '''' : true))' + - message: hostname must not be specified for protocols ['TCP', 'UDP'] + rule: 'self.all(l, l.protocol in [''TCP'', ''UDP''] ? (!has(l.hostname) + || l.hostname == '''') : true)' + - message: Listener name must be unique within the Gateway + rule: self.all(l1, self.exists_one(l2, l1.name == l2.name)) + - message: Combination of port, protocol and hostname must be unique + for each listener + rule: 'self.all(l1, self.exists_one(l2, l1.port == l2.port && l1.protocol + == l2.protocol && (has(l1.hostname) && has(l2.hostname) ? l1.hostname + == l2.hostname : !has(l1.hostname) && !has(l2.hostname))))' + tls: + description: |- + TLS specifies frontend and backend tls configuration for entire gateway. + + Support: Extended + properties: + backend: + description: |- + Backend describes TLS configuration for gateway when connecting + to backends. + + Note that this contains only details for the Gateway as a TLS client, + and does _not_ imply behavior about how to choose which backend should + get a TLS connection. That is determined by the presence of a BackendTLSPolicy. + + Support: Core + properties: + clientCertificateRef: + description: |- + ClientCertificateRef references an object that contains a client certificate + and its associated private key. It can reference standard Kubernetes resources, + i.e., Secret, or implementation-specific custom resources. + + A ClientCertificateRef is considered invalid if: + + * It refers to a resource that cannot be resolved (e.g., the referenced resource + does not exist) or is misconfigured (e.g., a Secret does not contain the keys + named `tls.crt` and `tls.key`). In this case, the `ResolvedRefs` condition + on the Gateway MUST be set to False with the Reason `InvalidClientCertificateRef` + and the Message of the Condition MUST indicate why the reference is invalid. + + * It refers to a resource in another namespace UNLESS there is a ReferenceGrant + in the target namespace that allows the certificate to be attached. + If a ReferenceGrant does not allow this reference, the `ResolvedRefs` condition + on the Gateway MUST be set to False with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the certificate + content (e.g., checking expiry or enforcing specific formats). In such cases, + an implementation-specific Reason and Message MUST be set. + + Support: Core - Reference to a Kubernetes TLS Secret (with the type `kubernetes.io/tls`). + Support: Implementation-specific - Other resource kinds or Secrets with a + different type (e.g., `Opaque`). + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For example + "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + type: object + frontend: + description: |- + Frontend describes TLS config when client connects to Gateway. + Support: Core + properties: + default: + description: |- + Default specifies the default client certificate validation configuration + for all Listeners handling HTTPS traffic, unless a per-port configuration + is defined. + + support: Core + properties: + validation: + description: |- + Validation holds configuration information for validating the frontend (client). + Setting this field will result in mutual authentication when connecting to the gateway. + In browsers this may result in a dialog appearing + that requests a user to specify the client certificate. + The maximum depth of a certificate chain accepted in verification is Implementation specific. + + Support: Core + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes + objects that contain a PEM-encoded TLS CA certificate bundle, which + is used as a trust anchor to validate the certificates presented by + the client. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the + referenced resource does not exist) or is misconfigured (e.g., a + ConfigMap does not contain a key named `ca.crt`). In this case, the + Reason on all matching HTTPS listeners must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this + case, the Reason on all matching HTTPS listeners must be set to + `InvalidCACertificateKind` and the Message of the Condition must explain + which kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace UNLESS there is a + ReferenceGrant in the target namespace that allows the CA + certificate to be attached. If a ReferenceGrant does not allow this + reference, the `ResolvedRefs` on all matching HTTPS listeners condition + MUST be set with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the + certificate content (e.g., checking expiry or enforcing specific formats). + In such cases, an implementation-specific Reason and Message MUST be set. + + In all cases, the implementation MUST ensure that the `ResolvedRefs` + condition is set to `status: False` on all targeted listeners (i.e., + listeners serving HTTPS on a matching port). The condition MUST + include a Reason and Message that indicate the cause of the error. If + ALL CACertificateRefs are invalid, the implementation MUST also ensure + the `Accepted` condition on the listener is set to `status: False`, with + the Reason `NoValidCACertificate`. + Implementations MAY choose to support attaching multiple CA certificates + to a listener, but this behavior is implementation-specific. + + Support: Core - A single reference to a Kubernetes ConfigMap, with the + CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + ObjectReference identifies an API object including its namespace. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When set to the empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "ConfigMap" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + mode: + default: AllowValidOnly + description: |- + FrontendValidationMode defines the mode for validating the client certificate. + There are two possible modes: + + - AllowValidOnly: In this mode, the gateway will accept connections only if + the client presents a valid certificate. This certificate must successfully + pass validation against the CA certificates specified in `CACertificateRefs`. + - AllowInsecureFallback: In this mode, the gateway will accept connections + even if the client certificate is not presented or fails verification. + + This approach delegates client authorization to the backend and introduce + a significant security risk. It should be used in testing environments or + on a temporary basis in non-testing environments. + + Defaults to AllowValidOnly. + + Support: Core + enum: + - AllowValidOnly + - AllowInsecureFallback + type: string + required: + - caCertificateRefs + type: object + type: object + perPort: + description: |- + PerPort specifies tls configuration assigned per port. + Per port configuration is optional. Once set this configuration overrides + the default configuration for all Listeners handling HTTPS traffic + that match this port. + Each override port requires a unique TLS configuration. + + support: Core + items: + properties: + port: + description: |- + The Port indicates the Port Number to which the TLS configuration will be + applied. This configuration will be applied to all Listeners handling HTTPS + traffic that match this port. + + Support: Core + format: int32 + maximum: 65535 + minimum: 1 + type: integer + tls: + description: |- + TLS store the configuration that will be applied to all Listeners handling + HTTPS traffic and matching given port. + + Support: Core + properties: + validation: + description: |- + Validation holds configuration information for validating the frontend (client). + Setting this field will result in mutual authentication when connecting to the gateway. + In browsers this may result in a dialog appearing + that requests a user to specify the client certificate. + The maximum depth of a certificate chain accepted in verification is Implementation specific. + + Support: Core + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes + objects that contain a PEM-encoded TLS CA certificate bundle, which + is used as a trust anchor to validate the certificates presented by + the client. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the + referenced resource does not exist) or is misconfigured (e.g., a + ConfigMap does not contain a key named `ca.crt`). In this case, the + Reason on all matching HTTPS listeners must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this + case, the Reason on all matching HTTPS listeners must be set to + `InvalidCACertificateKind` and the Message of the Condition must explain + which kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace UNLESS there is a + ReferenceGrant in the target namespace that allows the CA + certificate to be attached. If a ReferenceGrant does not allow this + reference, the `ResolvedRefs` on all matching HTTPS listeners condition + MUST be set with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the + certificate content (e.g., checking expiry or enforcing specific formats). + In such cases, an implementation-specific Reason and Message MUST be set. + + In all cases, the implementation MUST ensure that the `ResolvedRefs` + condition is set to `status: False` on all targeted listeners (i.e., + listeners serving HTTPS on a matching port). The condition MUST + include a Reason and Message that indicate the cause of the error. If + ALL CACertificateRefs are invalid, the implementation MUST also ensure + the `Accepted` condition on the listener is set to `status: False`, with + the Reason `NoValidCACertificate`. + Implementations MAY choose to support attaching multiple CA certificates + to a listener, but this behavior is implementation-specific. + + Support: Core - A single reference to a Kubernetes ConfigMap, with the + CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + ObjectReference identifies an API object including its namespace. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When set to the empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + For example "ConfigMap" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + mode: + default: AllowValidOnly + description: |- + FrontendValidationMode defines the mode for validating the client certificate. + There are two possible modes: + + - AllowValidOnly: In this mode, the gateway will accept connections only if + the client presents a valid certificate. This certificate must successfully + pass validation against the CA certificates specified in `CACertificateRefs`. + - AllowInsecureFallback: In this mode, the gateway will accept connections + even if the client certificate is not presented or fails verification. + + This approach delegates client authorization to the backend and introduce + a significant security risk. It should be used in testing environments or + on a temporary basis in non-testing environments. + + Defaults to AllowValidOnly. + + Support: Core + enum: + - AllowValidOnly + - AllowInsecureFallback + type: string + required: + - caCertificateRefs + type: object + type: object + required: + - port + - tls + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - port + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: Port for TLS configuration must be unique within + the Gateway + rule: self.all(t1, self.exists_one(t2, t1.port == t2.port)) + required: + - default + type: object + type: object + required: + - gatewayClassName + - listeners + type: object + status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: Status defines the current state of Gateway. + properties: + addresses: + description: |- + Addresses lists the network addresses that have been bound to the + Gateway. + + This list may differ from the addresses provided in the spec under some + conditions: + + * no addresses are specified, all addresses are dynamically assigned + * a combination of specified and dynamic addresses are assigned + * a specified address was unusable (e.g. already in use) + items: + description: GatewayStatusAddress describes a network address that + is bound to a Gateway. + oneOf: + - properties: + type: + enum: + - IPAddress + value: + anyOf: + - format: ipv4 + - format: ipv6 + - properties: + type: + not: + enum: + - IPAddress + properties: + type: + default: IPAddress + description: Type of the address. + maxLength: 253 + minLength: 1 + pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + value: + description: |- + Value of the address. The validity of the values will depend + on the type and support by the controller. + + Examples: `1.2.3.4`, `128::1`, `my-ip-address`. + maxLength: 253 + minLength: 1 + type: string + required: + - value + type: object + x-kubernetes-validations: + - message: Hostname value must only contain valid characters (matching + ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$) + rule: 'self.type == ''Hostname'' ? self.value.matches(r"""^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"""): + true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + attachedListenerSets: + description: |- + AttachedListenerSets represents the total number of ListenerSets that have been + successfully attached to this Gateway. + + A ListenerSet is successfully attached to a Gateway when all the following conditions are met: + - The ListenerSet is selected by the Gateway's AllowedListeners field + - The ListenerSet has a valid ParentRef selecting the Gateway + - The ListenerSet's status has the condition "Accepted: true" + + Uses for this field include troubleshooting AttachedListenerSets attachment and + measuring blast radius/impact of changes to a Gateway. + format: int32 + type: integer + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: |- + Conditions describe the current conditions of the Gateway. + + Implementations should prefer to express Gateway conditions + using the `GatewayConditionType` and `GatewayConditionReason` + constants so that operators and tools can converge on a common + vocabulary to describe Gateway state. + + Known condition types are: + + * "Accepted" + * "Programmed" + * "Ready" + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + listeners: + description: Listeners provide status for each unique listener port + defined in the Spec. + items: + description: ListenerStatus is the status associated with a Listener. + properties: + attachedRoutes: + description: |- + AttachedRoutes represents the total number of Routes that have been + successfully attached to this Listener. + + Successful attachment of a Route to a Listener is based solely on the + combination of the AllowedRoutes field on the corresponding Listener + and the Route's ParentRefs field. A Route is successfully attached to + a Listener when it is selected by the Listener's AllowedRoutes field + AND the Route has a valid ParentRef selecting the whole Gateway + resource or a specific Listener as a parent resource (more detail on + attachment semantics can be found in the documentation on the various + Route kinds ParentRefs fields). Listener or Route status does not impact + successful attachment, i.e. the AttachedRoutes field count MUST be set + for Listeners, even if the Accepted condition of an individual Listener is set + to "False". The AttachedRoutes number represents the number of Routes with + the Accepted condition set to "True" that have been attached to this Listener. + Routes with any other value for the Accepted condition MUST NOT be included + in this count. + + Uses for this field include troubleshooting Route attachment and + measuring blast radius/impact of changes to a Listener. + format: int32 + type: integer + conditions: + description: Conditions describe the current condition of this + listener. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + name: + description: Name is the name of the Listener that this status + corresponds to. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + supportedKinds: + description: |- + SupportedKinds is the list indicating the Kinds supported by this + listener. This MUST represent the kinds supported by an implementation for + that Listener configuration. + + If kinds are specified in Spec that are not supported, they MUST NOT + appear in this list and an implementation MUST set the "ResolvedRefs" + condition to "False" with the "InvalidRouteKinds" reason. If both valid + and invalid Route kinds are specified, the implementation MUST + reference the valid Route kinds that have been specified. + items: + description: RouteGroupKind indicates the group and kind of + a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + required: + - attachedRoutes + - conditions + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.gatewayClassName + name: Class + type: string + - jsonPath: .status.addresses[*].value + name: Address + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + Gateway represents an instance of a service-traffic handling infrastructure + by binding Listeners to a set of IP addresses. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of Gateway. + properties: + addresses: + description: |- + Addresses requested for this Gateway. This is optional and behavior can + depend on the implementation. If a value is set in the spec and the + requested address is invalid or unavailable, the implementation MUST + indicate this in an associated entry in GatewayStatus.Conditions. + + The Addresses field represents a request for the address(es) on the + "outside of the Gateway", that traffic bound for this Gateway will use. + This could be the IP address or hostname of an external load balancer or + other networking infrastructure, or some other address that traffic will + be sent to. + + If no Addresses are specified, the implementation MAY schedule the + Gateway in an implementation-specific manner, assigning an appropriate + set of Addresses. + + The implementation MUST bind all Listeners to every GatewayAddress that + it assigns to the Gateway and add a corresponding entry in + GatewayStatus.Addresses. + + Support: Extended + items: + description: GatewaySpecAddress describes an address that can be + bound to a Gateway. + oneOf: + - properties: + type: + enum: + - IPAddress + value: + anyOf: + - format: ipv4 + - format: ipv6 + - properties: + type: + not: + enum: + - IPAddress + properties: + type: + default: IPAddress + description: Type of the address. + maxLength: 253 + minLength: 1 + pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + value: + description: |- + When a value is unspecified, an implementation SHOULD automatically + assign an address matching the requested type if possible. + + If an implementation does not support an empty value, they MUST set the + "Programmed" condition in status to False with a reason of "AddressNotAssigned". + + Examples: `1.2.3.4`, `128::1`, `my-ip-address`. + maxLength: 253 + type: string + type: object + x-kubernetes-validations: + - message: Hostname value must be empty or contain only valid characters + (matching ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$) + rule: 'self.type == ''Hostname'' ? (!has(self.value) || self.value.matches(r"""^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$""")): + true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: IPAddress values must be unique + rule: 'self.all(a1, a1.type == ''IPAddress'' && has(a1.value) ? + self.exists_one(a2, a2.type == a1.type && has(a2.value) && a2.value + == a1.value) : true )' + - message: Hostname values must be unique + rule: 'self.all(a1, a1.type == ''Hostname'' && has(a1.value) ? + self.exists_one(a2, a2.type == a1.type && has(a2.value) && a2.value + == a1.value) : true )' + allowedListeners: + description: |- + AllowedListeners defines which ListenerSets can be attached to this Gateway. + The default value is to allow no ListenerSets. + properties: + namespaces: + default: + from: None + description: |- + Namespaces defines which namespaces ListenerSets can be attached to this Gateway. + The default value is to allow no ListenerSets. + properties: + from: + default: None + description: |- + From indicates where ListenerSets can attach to this Gateway. Possible + values are: + + * Same: Only ListenerSets in the same namespace may be attached to this Gateway. + * Selector: ListenerSets in namespaces selected by the selector may be attached to this Gateway. + * All: ListenerSets in all namespaces may be attached to this Gateway. + * None: Only listeners defined in the Gateway's spec are allowed + + The default value None + enum: + - All + - Selector + - Same + - None + type: string + selector: + description: |- + Selector must be specified when From is set to "Selector". In that case, + only ListenerSets in Namespaces matching this Selector will be selected by this + Gateway. This field is ignored for other values of "From". + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: object + gatewayClassName: + description: |- + GatewayClassName used for this Gateway. This is the name of a + GatewayClass resource. + maxLength: 253 + minLength: 1 + type: string + infrastructure: + description: |- + Infrastructure defines infrastructure level attributes about this Gateway instance. + + Support: Extended + properties: + annotations: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Annotations that SHOULD be applied to any resources created in response to this Gateway. + + For implementations creating other Kubernetes objects, this should be the `metadata.annotations` field on resources. + For other implementations, this refers to any relevant (implementation specific) "annotations" concepts. + + An implementation may chose to add additional implementation-specific annotations as they see fit. + + Support: Extended + maxProperties: 8 + type: object + x-kubernetes-validations: + - message: Annotation keys must be in the form of an optional + DNS subdomain prefix followed by a required name segment of + up to 63 characters. + rule: self.all(key, key.matches(r"""^([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_.]{0,61})?[A-Za-z0-9]$""")) + - message: If specified, the annotation key's prefix must be a + DNS subdomain not longer than 253 characters in total. + rule: self.all(key, key.split("/")[0].size() < 253) + labels: + additionalProperties: + description: |- + LabelValue is the value of a label in the Gateway API. This is used for validation + of maps such as Gateway infrastructure labels. This matches the Kubernetes + label validation rules: + * must be 63 characters or less (can be empty), + * unless empty, must begin and end with an alphanumeric character ([a-z0-9A-Z]), + * could contain dashes (-), underscores (_), dots (.), and alphanumerics between. + + Valid values include: + + * MyValue + * my.name + * 123-my-value + maxLength: 63 + minLength: 0 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + description: |- + Labels that SHOULD be applied to any resources created in response to this Gateway. + + For implementations creating other Kubernetes objects, this should be the `metadata.labels` field on resources. + For other implementations, this refers to any relevant (implementation specific) "labels" concepts. + + An implementation may chose to add additional implementation-specific labels as they see fit. + + If an implementation maps these labels to Pods, or any other resource that would need to be recreated when labels + change, it SHOULD clearly warn about this behavior in documentation. + + Support: Extended + maxProperties: 8 + type: object + x-kubernetes-validations: + - message: Label keys must be in the form of an optional DNS subdomain + prefix followed by a required name segment of up to 63 characters. + rule: self.all(key, key.matches(r"""^([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_.]{0,61})?[A-Za-z0-9]$""")) + - message: If specified, the label key's prefix must be a DNS + subdomain not longer than 253 characters in total. + rule: self.all(key, key.split("/")[0].size() < 253) + parametersRef: + description: |- + ParametersRef is a reference to a resource that contains the configuration + parameters corresponding to the Gateway. This is optional if the + controller does not require any additional configuration. + + This follows the same semantics as GatewayClass's `parametersRef`, but on a per-Gateway basis + + The Gateway's GatewayClass may provide its own `parametersRef`. When both are specified, + the merging behavior is implementation specific. + It is generally recommended that GatewayClass provides defaults that can be overridden by a Gateway. + + If the referent cannot be found, refers to an unsupported kind, or when + the data within that resource is malformed, the Gateway SHOULD be + rejected with the "Accepted" status condition set to "False" and an + "InvalidParameters" reason. + + Support: Implementation-specific + properties: + group: + description: Group is the group of the referent. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + type: object + listeners: + description: |- + Listeners associated with this Gateway. Listeners define + logical endpoints that are bound on this Gateway's addresses. + At least one Listener MUST be specified. + + ## Distinct Listeners + + Each Listener in a set of Listeners (for example, in a single Gateway) + MUST be _distinct_, in that a traffic flow MUST be able to be assigned to + exactly one listener. (This section uses "set of Listeners" rather than + "Listeners in a single Gateway" because implementations MAY merge configuration + from multiple Gateways onto a single data plane, and these rules _also_ + apply in that case). + + Practically, this means that each listener in a set MUST have a unique + combination of Port, Protocol, and, if supported by the protocol, Hostname. + + Some combinations of port, protocol, and TLS settings are considered + Core support and MUST be supported by implementations based on the objects + they support: + + HTTPRoute + + 1. HTTPRoute, Port: 80, Protocol: HTTP + 2. HTTPRoute, Port: 443, Protocol: HTTPS, TLS Mode: Terminate, TLS keypair provided + + TLSRoute + + 1. TLSRoute, Port: 443, Protocol: TLS, TLS Mode: Passthrough + + "Distinct" Listeners have the following property: + + **The implementation can match inbound requests to a single distinct + Listener**. + + When multiple Listeners share values for fields (for + example, two Listeners with the same Port value), the implementation + can match requests to only one of the Listeners using other + Listener fields. + + When multiple listeners have the same value for the Protocol field, then + each of the Listeners with matching Protocol values MUST have different + values for other fields. + + The set of fields that MUST be different for a Listener differs per protocol. + The following rules define the rules for what fields MUST be considered for + Listeners to be distinct with each protocol currently defined in the + Gateway API spec. + + The set of listeners that all share a protocol value MUST have _different_ + values for _at least one_ of these fields to be distinct: + + * **HTTP, HTTPS, TLS**: Port, Hostname + * **TCP, UDP**: Port + + One **very** important rule to call out involves what happens when an + implementation: + + * Supports TCP protocol Listeners, as well as HTTP, HTTPS, or TLS protocol + Listeners, and + * sees HTTP, HTTPS, or TLS protocols with the same `port` as one with TCP + Protocol. + + In this case all the Listeners that share a port with the + TCP Listener are not distinct and so MUST NOT be accepted. + + If an implementation does not support TCP Protocol Listeners, then the + previous rule does not apply, and the TCP Listeners SHOULD NOT be + accepted. + + Note that the `tls` field is not used for determining if a listener is distinct, because + Listeners that _only_ differ on TLS config will still conflict in all cases. + + ### Listeners that are distinct only by Hostname + + When the Listeners are distinct based only on Hostname, inbound request + hostnames MUST match from the most specific to least specific Hostname + values to choose the correct Listener and its associated set of Routes. + + Exact matches MUST be processed before wildcard matches, and wildcard + matches MUST be processed before fallback (empty Hostname value) + matches. For example, `"foo.example.com"` takes precedence over + `"*.example.com"`, and `"*.example.com"` takes precedence over `""`. + + Additionally, if there are multiple wildcard entries, more specific + wildcard entries must be processed before less specific wildcard entries. + For example, `"*.foo.example.com"` takes precedence over `"*.example.com"`. + + The precise definition here is that the higher the number of dots in the + hostname to the right of the wildcard character, the higher the precedence. + + The wildcard character will match any number of characters _and dots_ to + the left, however, so `"*.example.com"` will match both + `"foo.bar.example.com"` _and_ `"bar.example.com"`. + + ## Handling indistinct Listeners + + If a set of Listeners contains Listeners that are not distinct, then those + Listeners are _Conflicted_, and the implementation MUST set the "Conflicted" + condition in the Listener Status to "True". + + The words "indistinct" and "conflicted" are considered equivalent for the + purpose of this documentation. + + Implementations MAY choose to accept a Gateway with some Conflicted + Listeners only if they only accept the partial Listener set that contains + no Conflicted Listeners. + + Specifically, an implementation MAY accept a partial Listener set subject to + the following rules: + + * The implementation MUST NOT pick one conflicting Listener as the winner. + ALL indistinct Listeners must not be accepted for processing. + * At least one distinct Listener MUST be present, or else the Gateway effectively + contains _no_ Listeners, and must be rejected from processing as a whole. + + The implementation MUST set a "ListenersNotValid" condition on the + Gateway Status when the Gateway contains Conflicted Listeners whether or + not they accept the Gateway. That Condition SHOULD clearly + indicate in the Message which Listeners are conflicted, and which are + Accepted. Additionally, the Listener status for those listeners SHOULD + indicate which Listeners are conflicted and not Accepted. + + ## General Listener behavior + + Note that, for all distinct Listeners, requests SHOULD match at most one Listener. + For example, if Listeners are defined for "foo.example.com" and "*.example.com", a + request to "foo.example.com" SHOULD only be routed using routes attached + to the "foo.example.com" Listener (and not the "*.example.com" Listener). + + This concept is known as "Listener Isolation", and it is an Extended feature + of Gateway API. Implementations that do not support Listener Isolation MUST + clearly document this, and MUST NOT claim support for the + `GatewayHTTPListenerIsolation` feature. + + Implementations that _do_ support Listener Isolation SHOULD claim support + for the Extended `GatewayHTTPListenerIsolation` feature and pass the associated + conformance tests. + + ## Compatible Listeners + + A Gateway's Listeners are considered _compatible_ if: + + 1. They are distinct. + 2. The implementation can serve them in compliance with the Addresses + requirement that all Listeners are available on all assigned + addresses. + + Compatible combinations in Extended support are expected to vary across + implementations. A combination that is compatible for one implementation + may not be compatible for another. + + For example, an implementation that cannot serve both TCP and UDP listeners + on the same address, or cannot mix HTTPS and generic TLS listens on the same port + would not consider those cases compatible, even though they are distinct. + + Implementations MAY merge separate Gateways onto a single set of + Addresses if all Listeners across all Gateways are compatible. + + In a future release the MinItems=1 requirement MAY be dropped. + + Support: Core + items: + description: |- + Listener embodies the concept of a logical endpoint where a Gateway accepts + network connections. + properties: + allowedRoutes: + default: + namespaces: + from: Same + description: |- + AllowedRoutes defines the types of routes that MAY be attached to a + Listener and the trusted namespaces where those Route resources MAY be + present. + + Although a client request may match multiple route rules, only one rule + may ultimately receive the request. Matching precedence MUST be + determined in order of the following criteria: + + * The most specific match as defined by the Route type. + * The oldest Route based on creation timestamp. For example, a Route with + a creation timestamp of "2020-09-08 01:02:03" is given precedence over + a Route with a creation timestamp of "2020-09-08 01:02:04". + * If everything else is equivalent, the Route appearing first in + alphabetical order (namespace/name) should be given precedence. For + example, foo/bar is given precedence over foo/baz. + + All valid rules within a Route attached to this Listener should be + implemented. Invalid Route rules can be ignored (sometimes that will mean + the full Route). If a Route rule transitions from valid to invalid, + support for that Route rule should be dropped to ensure consistency. For + example, even if a filter specified by a Route rule is invalid, the rest + of the rules within that Route should still be supported. + + Support: Core + properties: + kinds: + description: |- + Kinds specifies the groups and kinds of Routes that are allowed to bind + to this Gateway Listener. When unspecified or empty, the kinds of Routes + selected are determined using the Listener protocol. + + A RouteGroupKind MUST correspond to kinds of Routes that are compatible + with the application protocol specified in the Listener's Protocol field. + If an implementation does not support or recognize this resource type, it + MUST set the "ResolvedRefs" condition to False for this Listener with the + "InvalidRouteKinds" reason. + + Support: Core + items: + description: RouteGroupKind indicates the group and kind + of a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + namespaces: + default: + from: Same + description: |- + Namespaces indicates namespaces from which Routes may be attached to this + Listener. This is restricted to the namespace of this Gateway by default. + + Support: Core + properties: + from: + default: Same + description: |- + From indicates where Routes will be selected for this Gateway. Possible + values are: + + * All: Routes in all namespaces may be used by this Gateway. + * Selector: Routes in namespaces selected by the selector may be used by + this Gateway. + * Same: Only Routes in the same namespace may be used by this Gateway. + + Support: Core + enum: + - All + - Selector + - Same + type: string + selector: + description: |- + Selector must be specified when From is set to "Selector". In that case, + only Routes in Namespaces matching this Selector will be selected by this + Gateway. This field is ignored for other values of "From". + + Support: Core + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: object + hostname: + description: |- + Hostname specifies the virtual hostname to match for protocol types that + define this concept. When unspecified, all hostnames are matched. This + field is ignored for protocols that don't require hostname based + matching. + + Implementations MUST apply Hostname matching appropriately for each of + the following protocols: + + * TLS: The Listener Hostname MUST match the SNI. + * HTTP: The Listener Hostname MUST match the Host header of the request. + * HTTPS: The Listener Hostname SHOULD match both the SNI and Host header. + Note that this does not require the SNI and Host header to be the same. + The semantics of this are described in more detail below. + + To ensure security, Section 11.1 of RFC-6066 emphasizes that server + implementations that rely on SNI hostname matching MUST also verify + hostnames within the application protocol. + + Section 9.1.2 of RFC-7540 provides a mechanism for servers to reject the + reuse of a connection by responding with the HTTP 421 Misdirected Request + status code. This indicates that the origin server has rejected the + request because it appears to have been misdirected. + + To detect misdirected requests, Gateways SHOULD match the authority of + the requests with all the SNI hostname(s) configured across all the + Gateway Listeners on the same port and protocol: + + * If another Listener has an exact match or more specific wildcard entry, + the Gateway SHOULD return a 421. + * If the current Listener (selected by SNI matching during ClientHello) + does not match the Host: + * If another Listener does match the Host, the Gateway SHOULD return a + 421. + * If no other Listener matches the Host, the Gateway MUST return a + 404. + + For HTTPRoute and TLSRoute resources, there is an interaction with the + `spec.hostnames` array. When both listener and route specify hostnames, + there MUST be an intersection between the values for a Route to be + accepted. For more information, refer to the Route specific Hostnames + documentation. + + Hostnames that are prefixed with a wildcard label (`*.`) are interpreted + as a suffix match. That means that a match for `*.example.com` would match + both `test.example.com`, and `foo.test.example.com`, but not `example.com`. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + name: + description: |- + Name is the name of the Listener. This name MUST be unique within a + Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + port: + description: |- + Port is the network port. Multiple listeners may use the + same port, subject to the Listener compatibility rules. + + Support: Core + format: int32 + maximum: 65535 + minimum: 1 + type: integer + protocol: + description: |- + Protocol specifies the network protocol this listener expects to receive. + + Support: Core + maxLength: 255 + minLength: 1 + pattern: ^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?$|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9]+$ + type: string + tls: + description: |- + TLS is the TLS configuration for the Listener. This field is required if + the Protocol field is "HTTPS" or "TLS". It is invalid to set this field + if the Protocol field is "HTTP", "TCP", or "UDP". + + The association of SNIs to Certificate defined in ListenerTLSConfig is + defined based on the Hostname field for this listener. + + The GatewayClass MUST use the longest matching SNI out of all + available certificates for any TLS handshake. + + Support: Core + properties: + certificateRefs: + description: |- + CertificateRefs contains a series of references to Kubernetes objects that + contains TLS certificates and private keys. These certificates are used to + establish a TLS handshake for requests that match the hostname of the + associated listener. + + A single CertificateRef to a Kubernetes Secret has "Core" support. + Implementations MAY choose to support attaching multiple certificates to + a Listener, but this behavior is implementation-specific. + + References to a resource in different namespace are invalid UNLESS there + is a ReferenceGrant in the target namespace that allows the certificate + to be attached. If a ReferenceGrant does not allow this reference, the + "ResolvedRefs" condition MUST be set to False for this listener with the + "RefNotPermitted" reason. + + This field is required to have at least one element when the mode is set + to "Terminate" (default) and is optional otherwise. + + CertificateRefs can reference to standard Kubernetes resources, i.e. + Secret, or implementation-specific custom resources. + + Support: Core - A single reference to a Kubernetes Secret of type kubernetes.io/tls + + Support: Implementation-specific (More than one reference or other resource types) + items: + description: |- + SecretObjectReference identifies an API object including its namespace, + defaulting to Secret. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For example + "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + mode: + default: Terminate + description: |- + Mode defines the TLS behavior for the TLS session initiated by the client. + There are two possible modes: + + - Terminate: The TLS session between the downstream client and the + Gateway is terminated at the Gateway. This mode requires certificates + to be specified in some way, such as populating the certificateRefs + field. + - Passthrough: The TLS session is NOT terminated by the Gateway. This + implies that the Gateway can't decipher the TLS stream except for + the ClientHello message of the TLS protocol. The certificateRefs field + is ignored in this mode. + + Support: Core + enum: + - Terminate + - Passthrough + type: string + options: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Options are a list of key/value pairs to enable extended TLS + configuration for each implementation. For example, configuring the + minimum TLS version or supported cipher suites. + + A set of common keys MAY be defined by the API in the future. To avoid + any ambiguity, implementation-specific definitions MUST use + domain-prefixed names, such as `example.com/my-custom-option`. + Un-prefixed names are reserved for key names defined by Gateway API. + + Support: Implementation-specific + maxProperties: 16 + type: object + type: object + x-kubernetes-validations: + - message: certificateRefs or options must be specified when + mode is Terminate + rule: 'self.mode == ''Terminate'' ? size(self.certificateRefs) + > 0 || size(self.options) > 0 : true' + required: + - name + - port + - protocol + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: tls must not be specified for protocols ['HTTP', 'TCP', + 'UDP'] + rule: 'self.all(l, l.protocol in [''HTTP'', ''TCP'', ''UDP''] ? + !has(l.tls) : true)' + - message: tls mode must be Terminate for protocol HTTPS + rule: 'self.all(l, (l.protocol == ''HTTPS'' && has(l.tls)) ? (l.tls.mode + == '''' || l.tls.mode == ''Terminate'') : true)' + - message: tls mode must be set for protocol TLS + rule: 'self.all(l, (l.protocol == ''TLS'' ? has(l.tls) && has(l.tls.mode) + && l.tls.mode != '''' : true))' + - message: hostname must not be specified for protocols ['TCP', 'UDP'] + rule: 'self.all(l, l.protocol in [''TCP'', ''UDP''] ? (!has(l.hostname) + || l.hostname == '''') : true)' + - message: Listener name must be unique within the Gateway + rule: self.all(l1, self.exists_one(l2, l1.name == l2.name)) + - message: Combination of port, protocol and hostname must be unique + for each listener + rule: 'self.all(l1, self.exists_one(l2, l1.port == l2.port && l1.protocol + == l2.protocol && (has(l1.hostname) && has(l2.hostname) ? l1.hostname + == l2.hostname : !has(l1.hostname) && !has(l2.hostname))))' + tls: + description: |- + TLS specifies frontend and backend tls configuration for entire gateway. + + Support: Extended + properties: + backend: + description: |- + Backend describes TLS configuration for gateway when connecting + to backends. + + Note that this contains only details for the Gateway as a TLS client, + and does _not_ imply behavior about how to choose which backend should + get a TLS connection. That is determined by the presence of a BackendTLSPolicy. + + Support: Core + properties: + clientCertificateRef: + description: |- + ClientCertificateRef references an object that contains a client certificate + and its associated private key. It can reference standard Kubernetes resources, + i.e., Secret, or implementation-specific custom resources. + + A ClientCertificateRef is considered invalid if: + + * It refers to a resource that cannot be resolved (e.g., the referenced resource + does not exist) or is misconfigured (e.g., a Secret does not contain the keys + named `tls.crt` and `tls.key`). In this case, the `ResolvedRefs` condition + on the Gateway MUST be set to False with the Reason `InvalidClientCertificateRef` + and the Message of the Condition MUST indicate why the reference is invalid. + + * It refers to a resource in another namespace UNLESS there is a ReferenceGrant + in the target namespace that allows the certificate to be attached. + If a ReferenceGrant does not allow this reference, the `ResolvedRefs` condition + on the Gateway MUST be set to False with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the certificate + content (e.g., checking expiry or enforcing specific formats). In such cases, + an implementation-specific Reason and Message MUST be set. + + Support: Core - Reference to a Kubernetes TLS Secret (with the type `kubernetes.io/tls`). + Support: Implementation-specific - Other resource kinds or Secrets with a + different type (e.g., `Opaque`). + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For example + "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + type: object + frontend: + description: |- + Frontend describes TLS config when client connects to Gateway. + Support: Core + properties: + default: + description: |- + Default specifies the default client certificate validation configuration + for all Listeners handling HTTPS traffic, unless a per-port configuration + is defined. + + support: Core + properties: + validation: + description: |- + Validation holds configuration information for validating the frontend (client). + Setting this field will result in mutual authentication when connecting to the gateway. + In browsers this may result in a dialog appearing + that requests a user to specify the client certificate. + The maximum depth of a certificate chain accepted in verification is Implementation specific. + + Support: Core + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes + objects that contain a PEM-encoded TLS CA certificate bundle, which + is used as a trust anchor to validate the certificates presented by + the client. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the + referenced resource does not exist) or is misconfigured (e.g., a + ConfigMap does not contain a key named `ca.crt`). In this case, the + Reason on all matching HTTPS listeners must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this + case, the Reason on all matching HTTPS listeners must be set to + `InvalidCACertificateKind` and the Message of the Condition must explain + which kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace UNLESS there is a + ReferenceGrant in the target namespace that allows the CA + certificate to be attached. If a ReferenceGrant does not allow this + reference, the `ResolvedRefs` on all matching HTTPS listeners condition + MUST be set with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the + certificate content (e.g., checking expiry or enforcing specific formats). + In such cases, an implementation-specific Reason and Message MUST be set. + + In all cases, the implementation MUST ensure that the `ResolvedRefs` + condition is set to `status: False` on all targeted listeners (i.e., + listeners serving HTTPS on a matching port). The condition MUST + include a Reason and Message that indicate the cause of the error. If + ALL CACertificateRefs are invalid, the implementation MUST also ensure + the `Accepted` condition on the listener is set to `status: False`, with + the Reason `NoValidCACertificate`. + Implementations MAY choose to support attaching multiple CA certificates + to a listener, but this behavior is implementation-specific. + + Support: Core - A single reference to a Kubernetes ConfigMap, with the + CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + ObjectReference identifies an API object including its namespace. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When set to the empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "ConfigMap" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + mode: + default: AllowValidOnly + description: |- + FrontendValidationMode defines the mode for validating the client certificate. + There are two possible modes: + + - AllowValidOnly: In this mode, the gateway will accept connections only if + the client presents a valid certificate. This certificate must successfully + pass validation against the CA certificates specified in `CACertificateRefs`. + - AllowInsecureFallback: In this mode, the gateway will accept connections + even if the client certificate is not presented or fails verification. + + This approach delegates client authorization to the backend and introduce + a significant security risk. It should be used in testing environments or + on a temporary basis in non-testing environments. + + Defaults to AllowValidOnly. + + Support: Core + enum: + - AllowValidOnly + - AllowInsecureFallback + type: string + required: + - caCertificateRefs + type: object + type: object + perPort: + description: |- + PerPort specifies tls configuration assigned per port. + Per port configuration is optional. Once set this configuration overrides + the default configuration for all Listeners handling HTTPS traffic + that match this port. + Each override port requires a unique TLS configuration. + + support: Core + items: + properties: + port: + description: |- + The Port indicates the Port Number to which the TLS configuration will be + applied. This configuration will be applied to all Listeners handling HTTPS + traffic that match this port. + + Support: Core + format: int32 + maximum: 65535 + minimum: 1 + type: integer + tls: + description: |- + TLS store the configuration that will be applied to all Listeners handling + HTTPS traffic and matching given port. + + Support: Core + properties: + validation: + description: |- + Validation holds configuration information for validating the frontend (client). + Setting this field will result in mutual authentication when connecting to the gateway. + In browsers this may result in a dialog appearing + that requests a user to specify the client certificate. + The maximum depth of a certificate chain accepted in verification is Implementation specific. + + Support: Core + properties: + caCertificateRefs: + description: |- + CACertificateRefs contains one or more references to Kubernetes + objects that contain a PEM-encoded TLS CA certificate bundle, which + is used as a trust anchor to validate the certificates presented by + the client. + + A CACertificateRef is invalid if: + + * It refers to a resource that cannot be resolved (e.g., the + referenced resource does not exist) or is misconfigured (e.g., a + ConfigMap does not contain a key named `ca.crt`). In this case, the + Reason on all matching HTTPS listeners must be set to `InvalidCACertificateRef` + and the Message of the Condition must indicate which reference is invalid and why. + + * It refers to an unknown or unsupported kind of resource. In this + case, the Reason on all matching HTTPS listeners must be set to + `InvalidCACertificateKind` and the Message of the Condition must explain + which kind of resource is unknown or unsupported. + + * It refers to a resource in another namespace UNLESS there is a + ReferenceGrant in the target namespace that allows the CA + certificate to be attached. If a ReferenceGrant does not allow this + reference, the `ResolvedRefs` on all matching HTTPS listeners condition + MUST be set with the Reason `RefNotPermitted`. + + Implementations MAY choose to perform further validation of the + certificate content (e.g., checking expiry or enforcing specific formats). + In such cases, an implementation-specific Reason and Message MUST be set. + + In all cases, the implementation MUST ensure that the `ResolvedRefs` + condition is set to `status: False` on all targeted listeners (i.e., + listeners serving HTTPS on a matching port). The condition MUST + include a Reason and Message that indicate the cause of the error. If + ALL CACertificateRefs are invalid, the implementation MUST also ensure + the `Accepted` condition on the listener is set to `status: False`, with + the Reason `NoValidCACertificate`. + Implementations MAY choose to support attaching multiple CA certificates + to a listener, but this behavior is implementation-specific. + + Support: Core - A single reference to a Kubernetes ConfigMap, with the + CA certificate in a key named `ca.crt`. + + Support: Implementation-specific - More than one reference, other kinds + of resources, or a single reference that includes multiple certificates. + items: + description: |- + ObjectReference identifies an API object including its namespace. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When set to the empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. + For example "ConfigMap" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + mode: + default: AllowValidOnly + description: |- + FrontendValidationMode defines the mode for validating the client certificate. + There are two possible modes: + + - AllowValidOnly: In this mode, the gateway will accept connections only if + the client presents a valid certificate. This certificate must successfully + pass validation against the CA certificates specified in `CACertificateRefs`. + - AllowInsecureFallback: In this mode, the gateway will accept connections + even if the client certificate is not presented or fails verification. + + This approach delegates client authorization to the backend and introduce + a significant security risk. It should be used in testing environments or + on a temporary basis in non-testing environments. + + Defaults to AllowValidOnly. + + Support: Core + enum: + - AllowValidOnly + - AllowInsecureFallback + type: string + required: + - caCertificateRefs + type: object + type: object + required: + - port + - tls + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - port + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: Port for TLS configuration must be unique within + the Gateway + rule: self.all(t1, self.exists_one(t2, t1.port == t2.port)) + required: + - default + type: object + type: object + required: + - gatewayClassName + - listeners + type: object + status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: Status defines the current state of Gateway. + properties: + addresses: + description: |- + Addresses lists the network addresses that have been bound to the + Gateway. + + This list may differ from the addresses provided in the spec under some + conditions: + + * no addresses are specified, all addresses are dynamically assigned + * a combination of specified and dynamic addresses are assigned + * a specified address was unusable (e.g. already in use) + items: + description: GatewayStatusAddress describes a network address that + is bound to a Gateway. + oneOf: + - properties: + type: + enum: + - IPAddress + value: + anyOf: + - format: ipv4 + - format: ipv6 + - properties: + type: + not: + enum: + - IPAddress + properties: + type: + default: IPAddress + description: Type of the address. + maxLength: 253 + minLength: 1 + pattern: ^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + value: + description: |- + Value of the address. The validity of the values will depend + on the type and support by the controller. + + Examples: `1.2.3.4`, `128::1`, `my-ip-address`. + maxLength: 253 + minLength: 1 + type: string + required: + - value + type: object + x-kubernetes-validations: + - message: Hostname value must only contain valid characters (matching + ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$) + rule: 'self.type == ''Hostname'' ? self.value.matches(r"""^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"""): + true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + attachedListenerSets: + description: |- + AttachedListenerSets represents the total number of ListenerSets that have been + successfully attached to this Gateway. + + A ListenerSet is successfully attached to a Gateway when all the following conditions are met: + - The ListenerSet is selected by the Gateway's AllowedListeners field + - The ListenerSet has a valid ParentRef selecting the Gateway + - The ListenerSet's status has the condition "Accepted: true" + + Uses for this field include troubleshooting AttachedListenerSets attachment and + measuring blast radius/impact of changes to a Gateway. + format: int32 + type: integer + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: |- + Conditions describe the current conditions of the Gateway. + + Implementations should prefer to express Gateway conditions + using the `GatewayConditionType` and `GatewayConditionReason` + constants so that operators and tools can converge on a common + vocabulary to describe Gateway state. + + Known condition types are: + + * "Accepted" + * "Programmed" + * "Ready" + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + listeners: + description: Listeners provide status for each unique listener port + defined in the Spec. + items: + description: ListenerStatus is the status associated with a Listener. + properties: + attachedRoutes: + description: |- + AttachedRoutes represents the total number of Routes that have been + successfully attached to this Listener. + + Successful attachment of a Route to a Listener is based solely on the + combination of the AllowedRoutes field on the corresponding Listener + and the Route's ParentRefs field. A Route is successfully attached to + a Listener when it is selected by the Listener's AllowedRoutes field + AND the Route has a valid ParentRef selecting the whole Gateway + resource or a specific Listener as a parent resource (more detail on + attachment semantics can be found in the documentation on the various + Route kinds ParentRefs fields). Listener or Route status does not impact + successful attachment, i.e. the AttachedRoutes field count MUST be set + for Listeners, even if the Accepted condition of an individual Listener is set + to "False". The AttachedRoutes number represents the number of Routes with + the Accepted condition set to "True" that have been attached to this Listener. + Routes with any other value for the Accepted condition MUST NOT be included + in this count. + + Uses for this field include troubleshooting Route attachment and + measuring blast radius/impact of changes to a Listener. + format: int32 + type: integer + conditions: + description: Conditions describe the current condition of this + listener. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + name: + description: Name is the name of the Listener that this status + corresponds to. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + supportedKinds: + description: |- + SupportedKinds is the list indicating the Kinds supported by this + listener. This MUST represent the kinds supported by an implementation for + that Listener configuration. + + If kinds are specified in Spec that are not supported, they MUST NOT + appear in this list and an implementation MUST set the "ResolvedRefs" + condition to "False" with the "InvalidRouteKinds" reason. If both valid + and invalid Route kinds are specified, the implementation MUST + reference the valid Route kinds that have been specified. + items: + description: RouteGroupKind indicates the group and kind of + a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + required: + - attachedRoutes + - conditions + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_grpcroutes.yaml b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_grpcroutes.yaml new file mode 100644 index 00000000..3920e60b --- /dev/null +++ b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_grpcroutes.yaml @@ -0,0 +1,2070 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.0 + gateway.networking.k8s.io/channel: standard + name: grpcroutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: GRPCRoute + listKind: GRPCRouteList + plural: grpcroutes + singular: grpcroute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + GRPCRoute provides a way to route gRPC requests. This includes the capability + to match requests by hostname, gRPC service, gRPC method, or HTTP/2 header. + Filters can be used to specify additional processing steps. Backends specify + where matching requests will be routed. + + GRPCRoute falls under extended support within the Gateway API. Within the + following specification, the word "MUST" indicates that an implementation + supporting GRPCRoute must conform to the indicated requirement, but an + implementation not supporting this route type need not follow the requirement + unless explicitly indicated. + + Implementations supporting `GRPCRoute` with the `HTTPS` `ProtocolType` MUST + accept HTTP/2 connections without an initial upgrade from HTTP/1.1, i.e. via + ALPN. If the implementation does not support this, then it MUST set the + "Accepted" condition to "False" for the affected listener with a reason of + "UnsupportedProtocol". Implementations MAY also accept HTTP/2 connections + with an upgrade from HTTP/1. + + Implementations supporting `GRPCRoute` with the `HTTP` `ProtocolType` MUST + support HTTP/2 over cleartext TCP (h2c, + https://www.rfc-editor.org/rfc/rfc7540#section-3.1) without an initial + upgrade from HTTP/1.1, i.e. with prior knowledge + (https://www.rfc-editor.org/rfc/rfc7540#section-3.4). If the implementation + does not support this, then it MUST set the "Accepted" condition to "False" + for the affected listener with a reason of "UnsupportedProtocol". + Implementations MAY also accept HTTP/2 connections with an upgrade from + HTTP/1, i.e. without prior knowledge. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of GRPCRoute. + properties: + hostnames: + description: |- + Hostnames defines a set of hostnames to match against the GRPC + Host header to select a GRPCRoute to process the request. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label MUST appear by itself as the first label. + + If a hostname is specified by both the Listener and GRPCRoute, there + MUST be at least one intersecting hostname for the GRPCRoute to be + attached to the Listener. For example: + + * A Listener with `test.example.com` as the hostname matches GRPCRoutes + that have either not specified any hostnames, or have specified at + least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches GRPCRoutes + that have either not specified any hostnames or have specified at least + one hostname that matches the Listener hostname. For example, + `test.example.com` and `*.example.com` would both match. On the other + hand, `example.com` and `test.example.net` would not match. + + Hostnames that are prefixed with a wildcard label (`*.`) are interpreted + as a suffix match. That means that a match for `*.example.com` would match + both `test.example.com`, and `foo.test.example.com`, but not `example.com`. + + If both the Listener and GRPCRoute have specified hostnames, any + GRPCRoute hostnames that do not match the Listener hostname MUST be + ignored. For example, if a Listener specified `*.example.com`, and the + GRPCRoute specified `test.example.com` and `test.example.net`, + `test.example.net` MUST NOT be considered for a match. + + If both the Listener and GRPCRoute have specified hostnames, and none + match with the criteria above, then the GRPCRoute MUST NOT be accepted by + the implementation. The implementation MUST raise an 'Accepted' Condition + with a status of `False` in the corresponding RouteParentStatus. + + If a Route (A) of type HTTPRoute or GRPCRoute is attached to a + Listener and that listener already has another Route (B) of the other + type attached and the intersection of the hostnames of A and B is + non-empty, then the implementation MUST accept exactly one of these two + routes, determined by the following criteria, in order: + + * The oldest Route based on creation timestamp. + * The Route appearing first in alphabetical order by + "{namespace}/{name}". + + The rejected Route MUST raise an 'Accepted' condition with a status of + 'False' in the corresponding RouteParentStatus. + + Support: Core + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + description: Rules are a list of GRPC matchers, filters and actions. + items: + description: |- + GRPCRouteRule defines the semantics for matching a gRPC request based on + conditions (matches), processing it (filters), and forwarding the request to + an API object (backendRefs). + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. + + Failure behavior here depends on how many BackendRefs are specified and + how many are invalid. + + If *all* entries in BackendRefs are invalid, and there are also no filters + specified in this route rule, *all* traffic which matches this rule MUST + receive an `UNAVAILABLE` status. + + See the GRPCBackendRef definition for the rules about what makes a single + GRPCBackendRef invalid. + + When a GRPCBackendRef is invalid, `UNAVAILABLE` statuses MUST be returned for + requests that would have otherwise been routed to an invalid backend. If + multiple backends are specified, and some are invalid, the proportion of + requests that would otherwise have been routed to an invalid backend + MUST receive an `UNAVAILABLE` status. + + For example, if two backends are specified with equal weights, and one is + invalid, 50 percent of traffic MUST receive an `UNAVAILABLE` status. + Implementations may choose how that 50 percent is determined. + + Support: Core for Kubernetes Service + + Support: Implementation-specific for any other resource + + Support for weight: Core + items: + description: |- + GRPCBackendRef defines how a GRPCRoute forwards a gRPC request. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + properties: + filters: + description: |- + Filters defined at this level MUST be executed if and only if the + request is being forwarded to the backend defined here. + + Support: Implementation-specific (For broader support of filters, use the + Filters field in GRPCRouteRule.) + items: + description: |- + GRPCRouteFilter defines processing steps that must be completed during the + request or response lifecycle. GRPCRouteFilters are meant as an extension + point to express processing that may be done in Gateway implementations. Some + examples include request or response modification, implementing + authentication strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type of the filter. + properties: + extensionRef: + description: |- + ExtensionRef is an optional, implementation-specific extension to the + "filter" behavior. For example, resource "myroutefilter" in group + "networking.example.net"). ExtensionRef MUST NOT be used for core and + extended filters. + + Support: Implementation-specific + + This filter can be used multiple times within the same rule. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: |- + RequestHeaderModifier defines a schema for a filter that modifies request + headers. + + Support: Core + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: |- + RequestMirror defines a schema for a filter that mirrors requests. + Requests are sent to the specified destination, but responses from + that destination are ignored. + + This filter can be used multiple times within the same rule. Note that + not all implementations will be able to support mirroring to multiple + backends. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef references a resource where mirrored requests are sent. + + Mirrored requests must be sent only to a single destination endpoint + within this BackendRef, irrespective of how many endpoints are present + within this BackendRef. + + If the referent cannot be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure the "ResolvedRefs" + condition on the Route status is set to `status: False` and not configure + this backend in the underlying implementation. + + If there is a cross-namespace reference to an *existing* object + that is not allowed by a ReferenceGrant, the controller must ensure the + "ResolvedRefs" condition on the Route is set to `status: False`, + with the "RefNotPermitted" reason and not configure this backend in the + underlying implementation. + + In either error case, the Message of the `ResolvedRefs` Condition + should be used to provide more detail about the problem. + + Support: Extended for Kubernetes Service + + Support: Implementation-specific for any other resource + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + fraction: + description: |- + Fraction represents the fraction of requests that should be + mirrored to BackendRef. + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal + to denominator + rule: self.numerator <= self.denominator + percent: + description: |- + Percent represents the percentage of requests that should be + mirrored to BackendRef. Its minimum value is 0 (indicating 0% of + requests) and its maximum value is 100 (indicating 100% of requests). + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + format: int32 + maximum: 100 + minimum: 0 + type: integer + required: + - backendRef + type: object + x-kubernetes-validations: + - message: Only one of percent or fraction may be + specified in HTTPRequestMirrorFilter + rule: '!(has(self.percent) && has(self.fraction))' + responseHeaderModifier: + description: |- + ResponseHeaderModifier defines a schema for a filter that modifies response + headers. + + Support: Extended + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: |- + Type identifies the type of filter to apply. As with other API fields, + types are classified into three conformance levels: + + - Core: Filter types and their corresponding configuration defined by + "Support: Core" in this package, e.g. "RequestHeaderModifier". All + implementations supporting GRPCRoute MUST support core filters. + + - Extended: Filter types and their corresponding configuration defined by + "Support: Extended" in this package, e.g. "RequestMirror". Implementers + are encouraged to support extended filters. + + - Implementation-specific: Filters that are defined and supported by specific vendors. + In the future, filters showing convergence in behavior across multiple + implementations will be considered for inclusion in extended or core + conformance levels. Filter-specific configuration for such filters + is specified using the ExtensionRef field. `Type` MUST be set to + "ExtensionRef" for custom filters. + + Implementers are encouraged to define custom implementation types to + extend the core API with implementation-specific behavior. + + If a reference to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have been processed by + that filter MUST receive a HTTP error response. + enum: + - ResponseHeaderModifier + - RequestHeaderModifier + - RequestMirror + - ExtensionRef + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: filter.requestHeaderModifier must be nil + if the filter.type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type + != ''RequestHeaderModifier'')' + - message: filter.requestHeaderModifier must be specified + for RequestHeaderModifier filter.type + rule: '!(!has(self.requestHeaderModifier) && self.type + == ''RequestHeaderModifier'')' + - message: filter.responseHeaderModifier must be nil + if the filter.type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type + != ''ResponseHeaderModifier'')' + - message: filter.responseHeaderModifier must be specified + for ResponseHeaderModifier filter.type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.requestMirror must be nil if the filter.type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.requestMirror must be specified for + RequestMirror filter.type + rule: '!(!has(self.requestMirror) && self.type == + ''RequestMirror'')' + - message: filter.extensionRef must be nil if the filter.type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.extensionRef must be specified for + ExtensionRef filter.type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: RequestHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'RequestHeaderModifier').size() + <= 1 + - message: ResponseHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'ResponseHeaderModifier').size() + <= 1 + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + filters: + description: |- + Filters define the filters that are applied to requests that match + this rule. + + The effects of ordering of multiple behaviors are currently unspecified. + This can change in the future based on feedback during the alpha stage. + + Conformance-levels at this level are defined based on the type of filter: + + - ALL core filters MUST be supported by all implementations that support + GRPCRoute. + - Implementers are encouraged to support extended filters. + - Implementation-specific custom filters have no API guarantees across + implementations. + + Specifying the same filter multiple times is not supported unless explicitly + indicated in the filter. + + If an implementation cannot support a combination of filters, it must clearly + document that limitation. In cases where incompatible or unsupported + filters are specified and cause the `Accepted` condition to be set to status + `False`, implementations may use the `IncompatibleFilters` reason to specify + this configuration error. + + Support: Core + items: + description: |- + GRPCRouteFilter defines processing steps that must be completed during the + request or response lifecycle. GRPCRouteFilters are meant as an extension + point to express processing that may be done in Gateway implementations. Some + examples include request or response modification, implementing + authentication strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type of the filter. + properties: + extensionRef: + description: |- + ExtensionRef is an optional, implementation-specific extension to the + "filter" behavior. For example, resource "myroutefilter" in group + "networking.example.net"). ExtensionRef MUST NOT be used for core and + extended filters. + + Support: Implementation-specific + + This filter can be used multiple times within the same rule. + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: |- + RequestHeaderModifier defines a schema for a filter that modifies request + headers. + + Support: Core + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: |- + RequestMirror defines a schema for a filter that mirrors requests. + Requests are sent to the specified destination, but responses from + that destination are ignored. + + This filter can be used multiple times within the same rule. Note that + not all implementations will be able to support mirroring to multiple + backends. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef references a resource where mirrored requests are sent. + + Mirrored requests must be sent only to a single destination endpoint + within this BackendRef, irrespective of how many endpoints are present + within this BackendRef. + + If the referent cannot be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure the "ResolvedRefs" + condition on the Route status is set to `status: False` and not configure + this backend in the underlying implementation. + + If there is a cross-namespace reference to an *existing* object + that is not allowed by a ReferenceGrant, the controller must ensure the + "ResolvedRefs" condition on the Route is set to `status: False`, + with the "RefNotPermitted" reason and not configure this backend in the + underlying implementation. + + In either error case, the Message of the `ResolvedRefs` Condition + should be used to provide more detail about the problem. + + Support: Extended for Kubernetes Service + + Support: Implementation-specific for any other resource + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + fraction: + description: |- + Fraction represents the fraction of requests that should be + mirrored to BackendRef. + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to + denominator + rule: self.numerator <= self.denominator + percent: + description: |- + Percent represents the percentage of requests that should be + mirrored to BackendRef. Its minimum value is 0 (indicating 0% of + requests) and its maximum value is 100 (indicating 100% of requests). + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + format: int32 + maximum: 100 + minimum: 0 + type: integer + required: + - backendRef + type: object + x-kubernetes-validations: + - message: Only one of percent or fraction may be specified + in HTTPRequestMirrorFilter + rule: '!(has(self.percent) && has(self.fraction))' + responseHeaderModifier: + description: |- + ResponseHeaderModifier defines a schema for a filter that modifies response + headers. + + Support: Extended + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: |- + Type identifies the type of filter to apply. As with other API fields, + types are classified into three conformance levels: + + - Core: Filter types and their corresponding configuration defined by + "Support: Core" in this package, e.g. "RequestHeaderModifier". All + implementations supporting GRPCRoute MUST support core filters. + + - Extended: Filter types and their corresponding configuration defined by + "Support: Extended" in this package, e.g. "RequestMirror". Implementers + are encouraged to support extended filters. + + - Implementation-specific: Filters that are defined and supported by specific vendors. + In the future, filters showing convergence in behavior across multiple + implementations will be considered for inclusion in extended or core + conformance levels. Filter-specific configuration for such filters + is specified using the ExtensionRef field. `Type` MUST be set to + "ExtensionRef" for custom filters. + + Implementers are encouraged to define custom implementation types to + extend the core API with implementation-specific behavior. + + If a reference to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have been processed by + that filter MUST receive a HTTP error response. + enum: + - ResponseHeaderModifier + - RequestHeaderModifier + - RequestMirror + - ExtensionRef + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: filter.requestHeaderModifier must be nil if the + filter.type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type != + ''RequestHeaderModifier'')' + - message: filter.requestHeaderModifier must be specified + for RequestHeaderModifier filter.type + rule: '!(!has(self.requestHeaderModifier) && self.type == + ''RequestHeaderModifier'')' + - message: filter.responseHeaderModifier must be nil if the + filter.type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type != + ''ResponseHeaderModifier'')' + - message: filter.responseHeaderModifier must be specified + for ResponseHeaderModifier filter.type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.requestMirror must be nil if the filter.type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.requestMirror must be specified for RequestMirror + filter.type + rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')' + - message: filter.extensionRef must be nil if the filter.type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.extensionRef must be specified for ExtensionRef + filter.type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: RequestHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'RequestHeaderModifier').size() + <= 1 + - message: ResponseHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'ResponseHeaderModifier').size() + <= 1 + matches: + description: |- + Matches define conditions used for matching the rule against incoming + gRPC requests. Each match is independent, i.e. this rule will be matched + if **any** one of the matches is satisfied. + + For example, take the following matches configuration: + + ``` + matches: + - method: + service: foo.bar + headers: + values: + version: 2 + - method: + service: foo.bar.v2 + ``` + + For a request to match against this rule, it MUST satisfy + EITHER of the two conditions: + + - service of foo.bar AND contains the header `version: 2` + - service of foo.bar.v2 + + See the documentation for GRPCRouteMatch on how to specify multiple + match conditions to be ANDed together. + + If no matches are specified, the implementation MUST match every gRPC request. + + Proxy or Load Balancer routing configuration generated from GRPCRoutes + MUST prioritize rules based on the following criteria, continuing on + ties. Merging MUST not be done between GRPCRoutes and HTTPRoutes. + Precedence MUST be given to the rule with the largest number of: + + * Characters in a matching non-wildcard hostname. + * Characters in a matching hostname. + * Characters in a matching service. + * Characters in a matching method. + * Header matches. + + If ties still exist across multiple Routes, matching precedence MUST be + determined in order of the following criteria, continuing on ties: + + * The oldest Route based on creation timestamp. + * The Route appearing first in alphabetical order by + "{namespace}/{name}". + + If ties still exist within the Route that has been given precedence, + matching precedence MUST be granted to the first matching rule meeting + the above criteria. + items: + description: |- + GRPCRouteMatch defines the predicate used to match requests to a given + action. Multiple match types are ANDed together, i.e. the match will + evaluate to true only if all conditions are satisfied. + + For example, the match below will match a gRPC request only if its service + is `foo` AND it contains the `version: v1` header: + + ``` + matches: + - method: + type: Exact + service: "foo" + - headers: + name: "version" + value "v1" + + ``` + properties: + headers: + description: |- + Headers specifies gRPC request header matchers. Multiple match values are + ANDed together, meaning, a request MUST match all the specified headers + to select the route. + items: + description: |- + GRPCHeaderMatch describes how to select a gRPC route by matching gRPC request + headers. + properties: + name: + description: |- + Name is the name of the gRPC Header to be matched. + + If multiple entries specify equivalent header names, only the first + entry with an equivalent name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: Type specifies how to match against + the value of the header. + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of the gRPC Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: |- + Method specifies a gRPC request service/method matcher. If this field is + not specified, all services and methods will match. + properties: + method: + description: |- + Value of the method to match against. If left empty or omitted, will + match all services. + + At least one of Service and Method MUST be a non-empty string. + maxLength: 1024 + type: string + service: + description: |- + Value of the service to match against. If left empty or omitted, will + match any service. + + At least one of Service and Method MUST be a non-empty string. + maxLength: 1024 + type: string + type: + default: Exact + description: |- + Type specifies how to match against the service and/or method. + Support: Core (Exact with service and method specified) + + Support: Implementation-specific (Exact with method specified but no service specified) + + Support: Implementation-specific (RegularExpression) + enum: + - Exact + - RegularExpression + type: string + type: object + x-kubernetes-validations: + - message: One or both of 'service' or 'method' must be + specified + rule: 'has(self.type) ? has(self.service) || has(self.method) + : true' + - message: service must only contain valid characters + (matching ^(?i)\.?[a-z_][a-z_0-9]*(\.[a-z_][a-z_0-9]*)*$) + rule: '(!has(self.type) || self.type == ''Exact'') && + has(self.service) ? self.service.matches(r"""^(?i)\.?[a-z_][a-z_0-9]*(\.[a-z_][a-z_0-9]*)*$"""): + true' + - message: method must only contain valid characters (matching + ^[A-Za-z_][A-Za-z_0-9]*$) + rule: '(!has(self.type) || self.type == ''Exact'') && + has(self.method) ? self.method.matches(r"""^[A-Za-z_][A-Za-z_0-9]*$"""): + true' + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + name: + description: |- + Name is the name of the route rule. This name MUST be unique within a Route if it is set. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: object + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: While 16 rules and 64 matches per rule are allowed, the + total number of matches across all rules in a route must be less + than 128 + rule: '(self.size() > 0 ? (has(self[0].matches) ? self[0].matches.size() + : 0) : 0) + (self.size() > 1 ? (has(self[1].matches) ? self[1].matches.size() + : 0) : 0) + (self.size() > 2 ? (has(self[2].matches) ? self[2].matches.size() + : 0) : 0) + (self.size() > 3 ? (has(self[3].matches) ? self[3].matches.size() + : 0) : 0) + (self.size() > 4 ? (has(self[4].matches) ? self[4].matches.size() + : 0) : 0) + (self.size() > 5 ? (has(self[5].matches) ? self[5].matches.size() + : 0) : 0) + (self.size() > 6 ? (has(self[6].matches) ? self[6].matches.size() + : 0) : 0) + (self.size() > 7 ? (has(self[7].matches) ? self[7].matches.size() + : 0) : 0) + (self.size() > 8 ? (has(self[8].matches) ? self[8].matches.size() + : 0) : 0) + (self.size() > 9 ? (has(self[9].matches) ? self[9].matches.size() + : 0) : 0) + (self.size() > 10 ? (has(self[10].matches) ? self[10].matches.size() + : 0) : 0) + (self.size() > 11 ? (has(self[11].matches) ? self[11].matches.size() + : 0) : 0) + (self.size() > 12 ? (has(self[12].matches) ? self[12].matches.size() + : 0) : 0) + (self.size() > 13 ? (has(self[13].matches) ? self[13].matches.size() + : 0) : 0) + (self.size() > 14 ? (has(self[14].matches) ? self[14].matches.size() + : 0) : 0) + (self.size() > 15 ? (has(self[15].matches) ? self[15].matches.size() + : 0) : 0) <= 128' + type: object + status: + description: Status defines the current state of GRPCRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_httproutes.yaml b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_httproutes.yaml new file mode 100644 index 00000000..3c52501a --- /dev/null +++ b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_httproutes.yaml @@ -0,0 +1,6921 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.0 + gateway.networking.k8s.io/channel: standard + name: httproutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: HTTPRoute + listKind: HTTPRouteList + plural: httproutes + singular: httproute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + HTTPRoute provides a way to route HTTP requests. This includes the capability + to match requests by hostname, path, header, or query param. Filters can be + used to specify additional processing steps. Backends specify where matching + requests should be routed. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: |- + Hostnames defines a set of hostnames that should match against the HTTP Host + header to select a HTTPRoute used to process the request. Implementations + MUST ignore any port value specified in the HTTP Host header while + performing a match and (absent of any applicable header modification + configuration) MUST forward this header unmodified to the backend. + + Valid values for Hostnames are determined by RFC 1123 definition of a + hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + If a hostname is specified by both the Listener and HTTPRoute, there + must be at least one intersecting hostname for the HTTPRoute to be + attached to the Listener. For example: + + * A Listener with `test.example.com` as the hostname matches HTTPRoutes + that have either not specified any hostnames, or have specified at + least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + that have either not specified any hostnames or have specified at least + one hostname that matches the Listener hostname. For example, + `*.example.com`, `test.example.com`, and `foo.test.example.com` would + all match. On the other hand, `example.com` and `test.example.net` would + not match. + + Hostnames that are prefixed with a wildcard label (`*.`) are interpreted + as a suffix match. That means that a match for `*.example.com` would match + both `test.example.com`, and `foo.test.example.com`, but not `example.com`. + + If both the Listener and HTTPRoute have specified hostnames, any + HTTPRoute hostnames that do not match the Listener hostname MUST be + ignored. For example, if a Listener specified `*.example.com`, and the + HTTPRoute specified `test.example.com` and `test.example.net`, + `test.example.net` must not be considered for a match. + + If both the Listener and HTTPRoute have specified hostnames, and none + match with the criteria above, then the HTTPRoute is not accepted. The + implementation must raise an 'Accepted' Condition with a status of + `False` in the corresponding RouteParentStatus. + + In the event that multiple HTTPRoutes specify intersecting hostnames (e.g. + overlapping wildcard matching and exact matching hostnames), precedence must + be given to rules from the HTTPRoute with the largest number of: + + * Characters in a matching non-wildcard hostname. + * Characters in a matching hostname. + + If ties exist across multiple Routes, the matching precedence rules for + HTTPRouteMatches takes over. + + Support: Core + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: |- + HTTPRouteRule defines semantics for matching an HTTP request based on + conditions (matches), processing it (filters), and forwarding the request to + an API object (backendRefs). + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. + + Failure behavior here depends on how many BackendRefs are specified and + how many are invalid. + + If *all* entries in BackendRefs are invalid, and there are also no filters + specified in this route rule, *all* traffic which matches this rule MUST + receive a 500 status code. + + See the HTTPBackendRef definition for the rules about what makes a single + HTTPBackendRef invalid. + + When a HTTPBackendRef is invalid, 500 status codes MUST be returned for + requests that would have otherwise been routed to an invalid backend. If + multiple backends are specified, and some are invalid, the proportion of + requests that would otherwise have been routed to an invalid backend + MUST receive a 500 status code. + + For example, if two backends are specified with equal weights, and one is + invalid, 50 percent of traffic must receive a 500. Implementations may + choose how that 50 percent is determined. + + When a HTTPBackendRef refers to a Service that has no ready endpoints, + implementations SHOULD return a 503 for requests to that backend instead. + If an implementation chooses to do this, all of the above rules for 500 responses + MUST also apply for responses that return a 503. + + Support: Core for Kubernetes Service + + Support: Extended for Kubernetes ServiceImport + + Support: Implementation-specific for any other resource + + Support for weight: Core + items: + description: |- + HTTPBackendRef defines how a HTTPRoute forwards a HTTP request. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + properties: + filters: + description: |- + Filters defined at this level should be executed if and only if the + request is being forwarded to the backend defined here. + + Support: Implementation-specific (For broader support of filters, use the + Filters field in HTTPRouteRule.) + items: + description: |- + HTTPRouteFilter defines processing steps that must be completed during the + request or response lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway implementations. Some + examples include request or response modification, implementing + authentication strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type of the filter. + properties: + cors: + description: |- + CORS defines a schema for a filter that responds to the + cross-origin request based on HTTP response header. + + Support: Extended + properties: + allowCredentials: + description: |- + AllowCredentials indicates whether the actual cross-origin request allows + to include credentials. + + When set to true, the gateway will include the `Access-Control-Allow-Credentials` + response header with value true (case-sensitive). + + When set to false or omitted the gateway will omit the header + `Access-Control-Allow-Credentials` entirely (this is the standard CORS + behavior). + + Support: Extended + type: boolean + allowHeaders: + description: |- + AllowHeaders indicates which HTTP request headers are supported for + accessing the requested resource. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Allow-Headers` + response header are separated by a comma (","). + + When the `AllowHeaders` field is configured with one or more headers, the + gateway must return the `Access-Control-Allow-Headers` response header + which value is present in the `AllowHeaders` field. + + If any header name in the `Access-Control-Request-Headers` request header + is not included in the list of header names specified by the response + header `Access-Control-Allow-Headers`, it will present an error on the + client side. + + If any header name in the `Access-Control-Allow-Headers` response header + does not recognize by the client, it will also occur an error on the + client side. + + A wildcard indicates that the requests with all HTTP headers are allowed. + If config contains the wildcard "*" in allowHeaders and the request is + not credentialed, the `Access-Control-Allow-Headers` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Headers from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Headers` response header. When + also the `AllowCredentials` field is true and `AllowHeaders` field + is specified with the `*` wildcard, the gateway must specify one or more + HTTP headers in the value of the `Access-Control-Allow-Headers` response + header. The value of the header `Access-Control-Allow-Headers` is same as + the `Access-Control-Request-Headers` header provided by the client. If + the header `Access-Control-Request-Headers` is not included in the + request, the gateway will omit the `Access-Control-Allow-Headers` + response header, instead of specifying the `*` wildcard. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowHeaders cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowMethods: + description: |- + AllowMethods indicates which HTTP methods are supported for accessing the + requested resource. + + Valid values are any method defined by RFC9110, along with the special + value `*`, which represents all HTTP methods are allowed. + + Method names are case-sensitive, so these values are also case-sensitive. + (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) + + Multiple method names in the value of the `Access-Control-Allow-Methods` + response header are separated by a comma (","). + + A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. + (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The + CORS-safelisted methods are always allowed, regardless of whether they + are specified in the `AllowMethods` field. + + When the `AllowMethods` field is configured with one or more methods, the + gateway must return the `Access-Control-Allow-Methods` response header + which value is present in the `AllowMethods` field. + + If the HTTP method of the `Access-Control-Request-Method` request header + is not included in the list of methods specified by the response header + `Access-Control-Allow-Methods`, it will present an error on the client + side. + + If config contains the wildcard "*" in allowMethods and the request is + not credentialed, the `Access-Control-Allow-Methods` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Method from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Methods` response header. When + also the `AllowCredentials` field is true and `AllowMethods` field + specified with the `*` wildcard, the gateway must specify one HTTP method + in the value of the Access-Control-Allow-Methods response header. The + value of the header `Access-Control-Allow-Methods` is same as the + `Access-Control-Request-Method` header provided by the client. If the + header `Access-Control-Request-Method` is not included in the request, + the gateway will omit the `Access-Control-Allow-Methods` response header, + instead of specifying the `*` wildcard. + + Support: Extended + items: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + - '*' + type: string + maxItems: 9 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowMethods cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowOrigins: + description: |- + AllowOrigins indicates whether the response can be shared with requested + resource from the given `Origin`. + + The `Origin` consists of a scheme and a host, with an optional port, and + takes the form `<scheme>://<host>(:<port>)`. + + Valid values for scheme are: `http` and `https`. + + Valid values for port are any integer between 1 and 65535 (the list of + available TCP/UDP ports). Note that, if not included, port `80` is + assumed for `http` scheme origins, and port `443` is assumed for `https` + origins. This may affect origin matching. + + The host part of the origin may contain the wildcard character `*`. These + wildcard characters behave as follows: + + * `*` is a greedy match to the _left_, including any number of + DNS labels to the left of its position. This also means that + `*` will include any number of period `.` characters to the + left of its position. + * A wildcard by itself matches all hosts. + + An origin value that includes _only_ the `*` character indicates requests + from all `Origin`s are allowed. + + When the `AllowOrigins` field is configured with multiple origins, it + means the server supports clients from multiple origins. If the request + `Origin` matches the configured allowed origins, the gateway must return + the given `Origin` and sets value of the header + `Access-Control-Allow-Origin` same as the `Origin` header provided by the + client. + + The status code of a successful response to a "preflight" request is + always an OK status (i.e., 204 or 200). + + If the request `Origin` does not match the configured allowed origins, + the gateway returns 204/200 response but doesn't set the relevant + cross-origin response headers. Alternatively, the gateway responds with + 403 status to the "preflight" request is denied, coupled with omitting + the CORS headers. The cross-origin request fails on the client side. + Therefore, the client doesn't attempt the actual cross-origin request. + + Conversely, if the request `Origin` matches one of the configured + allowed origins, the gateway sets the response header + `Access-Control-Allow-Origin` to the same value as the `Origin` + header provided by the client. + + When config has the wildcard ("*") in allowOrigins, and the request + is not credentialed (e.g., it is a preflight request), the + `Access-Control-Allow-Origin` response header either contains the + wildcard as well or the Origin from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Origin` response header. When + also the `AllowCredentials` field is true and `AllowOrigins` field + specified with the `*` wildcard, the gateway must return a single origin + in the value of the `Access-Control-Allow-Origin` response header, + instead of specifying the `*` wildcard. The value of the header + `Access-Control-Allow-Origin` is same as the `Origin` header provided by + the client. + + Support: Extended + items: + description: |- + The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and + encoding rules specified in RFC3986. The CORSOrigin MUST include both a + scheme ("http" or "https") and a scheme-specific-part, or it should be a single '*' character. + URIs that include an authority MUST include a fully qualified domain name or + IP address as the host. + maxLength: 253 + minLength: 1 + pattern: (^\*$)|(^(http(s)?):\/\/(((\*\.)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9-]+|\*)(:([0-9]{1,5}))?)$) + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowOrigins cannot contain '*' alongside + other origins + rule: '!(''*'' in self && self.size() > 1)' + exposeHeaders: + description: |- + ExposeHeaders indicates which HTTP response headers can be exposed + to client-side scripts in response to a cross-origin request. + + A CORS-safelisted response header is an HTTP header in a CORS response + that it is considered safe to expose to the client scripts. + The CORS-safelisted response headers include the following headers: + `Cache-Control` + `Content-Language` + `Content-Length` + `Content-Type` + `Expires` + `Last-Modified` + `Pragma` + (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) + The CORS-safelisted response headers are exposed to client by default. + + When an HTTP header name is specified using the `ExposeHeaders` field, + this additional header will be exposed as part of the response to the + client. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Expose-Headers` + response header are separated by a comma (","). + + A wildcard indicates that the responses with all HTTP headers are exposed + to clients. The `Access-Control-Expose-Headers` response header can only + use `*` wildcard as value when the request is not credentialed. + + When the `exposeHeaders` config field contains the "*" wildcard and + the request is credentialed, the gateway cannot use the `*` wildcard in + the `Access-Control-Expose-Headers` response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + maxAge: + default: 5 + description: |- + MaxAge indicates the duration (in seconds) for the client to cache the + results of a "preflight" request. + + The information provided by the `Access-Control-Allow-Methods` and + `Access-Control-Allow-Headers` response headers can be cached by the + client until the time specified by `Access-Control-Max-Age` elapses. + + The default value of `Access-Control-Max-Age` response header is 5 + (seconds). + + When the `MaxAge` field is unspecified, the gateway sets the response + header "Access-Control-Max-Age: 5" by default. + format: int32 + minimum: 1 + type: integer + type: object + extensionRef: + description: |- + ExtensionRef is an optional, implementation-specific extension to the + "filter" behavior. For example, resource "myroutefilter" in group + "networking.example.net"). ExtensionRef MUST NOT be used for core and + extended filters. + + This filter can be used multiple times within the same rule. + + Support: Implementation-specific + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: |- + RequestHeaderModifier defines a schema for a filter that modifies request + headers. + + Support: Core + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: |- + RequestMirror defines a schema for a filter that mirrors requests. + Requests are sent to the specified destination, but responses from + that destination are ignored. + + This filter can be used multiple times within the same rule. Note that + not all implementations will be able to support mirroring to multiple + backends. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef references a resource where mirrored requests are sent. + + Mirrored requests must be sent only to a single destination endpoint + within this BackendRef, irrespective of how many endpoints are present + within this BackendRef. + + If the referent cannot be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure the "ResolvedRefs" + condition on the Route status is set to `status: False` and not configure + this backend in the underlying implementation. + + If there is a cross-namespace reference to an *existing* object + that is not allowed by a ReferenceGrant, the controller must ensure the + "ResolvedRefs" condition on the Route is set to `status: False`, + with the "RefNotPermitted" reason and not configure this backend in the + underlying implementation. + + In either error case, the Message of the `ResolvedRefs` Condition + should be used to provide more detail about the problem. + + Support: Extended for Kubernetes Service + + Support: Implementation-specific for any other resource + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + fraction: + description: |- + Fraction represents the fraction of requests that should be + mirrored to BackendRef. + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal + to denominator + rule: self.numerator <= self.denominator + percent: + description: |- + Percent represents the percentage of requests that should be + mirrored to BackendRef. Its minimum value is 0 (indicating 0% of + requests) and its maximum value is 100 (indicating 100% of requests). + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + format: int32 + maximum: 100 + minimum: 0 + type: integer + required: + - backendRef + type: object + x-kubernetes-validations: + - message: Only one of percent or fraction may be + specified in HTTPRequestMirrorFilter + rule: '!(has(self.percent) && has(self.fraction))' + requestRedirect: + description: |- + RequestRedirect defines a schema for a filter that responds to the + request with an HTTP redirection. + + Support: Core + properties: + hostname: + description: |- + Hostname is the hostname to be used in the value of the `Location` + header in the response. + When empty, the hostname in the `Host` header of the request is used. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines parameters used to modify the path of the incoming request. + The modified path is then used to construct the `Location` header. When + empty, the request path is used as-is. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified + when type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified + when type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + port: + description: |- + Port is the port to be used in the value of the `Location` + header in the response. + + If no port is specified, the redirect port MUST be derived using the + following rules: + + * If redirect scheme is not-empty, the redirect port MUST be the well-known + port associated with the redirect scheme. Specifically "http" to port 80 + and "https" to port 443. If the redirect scheme does not have a + well-known port, the listener port of the Gateway SHOULD be used. + * If redirect scheme is empty, the redirect port MUST be the Gateway + Listener port. + + Implementations SHOULD NOT add the port number in the 'Location' + header in the following cases: + + * A Location header that will use HTTP (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 80. + * A Location header that will use HTTPS (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 443. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: |- + Scheme is the scheme to be used in the value of the `Location` header in + the response. When empty, the scheme of the request is used. + + Scheme redirects can affect the port of the redirect, for more information, + refer to the documentation for the port field of this filter. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Extended + enum: + - http + - https + type: string + statusCode: + default: 302 + description: |- + StatusCode is the HTTP status code to be used in response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Core + enum: + - 301 + - 302 + - 303 + - 307 + - 308 + type: integer + type: object + responseHeaderModifier: + description: |- + ResponseHeaderModifier defines a schema for a filter that modifies response + headers. + + Support: Extended + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: |- + Type identifies the type of filter to apply. As with other API fields, + types are classified into three conformance levels: + + - Core: Filter types and their corresponding configuration defined by + "Support: Core" in this package, e.g. "RequestHeaderModifier". All + implementations must support core filters. + + - Extended: Filter types and their corresponding configuration defined by + "Support: Extended" in this package, e.g. "RequestMirror". Implementers + are encouraged to support extended filters. + + - Implementation-specific: Filters that are defined and supported by + specific vendors. + In the future, filters showing convergence in behavior across multiple + implementations will be considered for inclusion in extended or core + conformance levels. Filter-specific configuration for such filters + is specified using the ExtensionRef field. `Type` should be set to + "ExtensionRef" for custom filters. + + Implementers are encouraged to define custom implementation types to + extend the core API with implementation-specific behavior. + + If a reference to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have been processed by + that filter MUST receive a HTTP error response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + - CORS + type: string + urlRewrite: + description: |- + URLRewrite defines a schema for a filter that modifies a request during forwarding. + + Support: Extended + properties: + hostname: + description: |- + Hostname is the value to be used to replace the Host header value during + forwarding. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines a path rewrite. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified + when type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified + when type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + type: object + required: + - type + type: object + x-kubernetes-validations: + - message: filter.cors must be nil if the filter.type + is not CORS + rule: '!(has(self.cors) && self.type != ''CORS'')' + - message: filter.cors must be specified for CORS filter.type + rule: '!(!has(self.cors) && self.type == ''CORS'')' + - message: filter.requestHeaderModifier must be nil + if the filter.type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type + != ''RequestHeaderModifier'')' + - message: filter.requestHeaderModifier must be specified + for RequestHeaderModifier filter.type + rule: '!(!has(self.requestHeaderModifier) && self.type + == ''RequestHeaderModifier'')' + - message: filter.responseHeaderModifier must be nil + if the filter.type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type + != ''ResponseHeaderModifier'')' + - message: filter.responseHeaderModifier must be specified + for ResponseHeaderModifier filter.type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.requestMirror must be nil if the filter.type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.requestMirror must be specified for + RequestMirror filter.type + rule: '!(!has(self.requestMirror) && self.type == + ''RequestMirror'')' + - message: filter.requestRedirect must be nil if the + filter.type is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != + ''RequestRedirect'')' + - message: filter.requestRedirect must be specified + for RequestRedirect filter.type + rule: '!(!has(self.requestRedirect) && self.type == + ''RequestRedirect'')' + - message: filter.urlRewrite must be nil if the filter.type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.urlRewrite must be specified for URLRewrite + filter.type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.extensionRef must be nil if the filter.type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.extensionRef must be specified for + ExtensionRef filter.type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') + && self.exists(f, f.type == ''URLRewrite''))' + - message: RequestHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'RequestHeaderModifier').size() + <= 1 + - message: ResponseHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'ResponseHeaderModifier').size() + <= 1 + - message: RequestRedirect filter cannot be repeated + rule: self.filter(f, f.type == 'RequestRedirect').size() + <= 1 + - message: URLRewrite filter cannot be repeated + rule: self.filter(f, f.type == 'URLRewrite').size() + <= 1 + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + filters: + description: |- + Filters define the filters that are applied to requests that match + this rule. + + Wherever possible, implementations SHOULD implement filters in the order + they are specified. + + Implementations MAY choose to implement this ordering strictly, rejecting + any combination or order of filters that cannot be supported. If implementations + choose a strict interpretation of filter ordering, they MUST clearly document + that behavior. + + To reject an invalid combination or order of filters, implementations SHOULD + consider the Route Rules with this configuration invalid. If all Route Rules + in a Route are invalid, the entire Route would be considered invalid. If only + a portion of Route Rules are invalid, implementations MUST set the + "PartiallyInvalid" condition for the Route. + + Conformance-levels at this level are defined based on the type of filter: + + - ALL core filters MUST be supported by all implementations. + - Implementers are encouraged to support extended filters. + - Implementation-specific custom filters have no API guarantees across + implementations. + + Specifying the same filter multiple times is not supported unless explicitly + indicated in the filter. + + All filters are expected to be compatible with each other except for the + URLRewrite and RequestRedirect filters, which may not be combined. If an + implementation cannot support other combinations of filters, they must clearly + document that limitation. In cases where incompatible or unsupported + filters are specified and cause the `Accepted` condition to be set to status + `False`, implementations may use the `IncompatibleFilters` reason to specify + this configuration error. + + Support: Core + items: + description: |- + HTTPRouteFilter defines processing steps that must be completed during the + request or response lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway implementations. Some + examples include request or response modification, implementing + authentication strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type of the filter. + properties: + cors: + description: |- + CORS defines a schema for a filter that responds to the + cross-origin request based on HTTP response header. + + Support: Extended + properties: + allowCredentials: + description: |- + AllowCredentials indicates whether the actual cross-origin request allows + to include credentials. + + When set to true, the gateway will include the `Access-Control-Allow-Credentials` + response header with value true (case-sensitive). + + When set to false or omitted the gateway will omit the header + `Access-Control-Allow-Credentials` entirely (this is the standard CORS + behavior). + + Support: Extended + type: boolean + allowHeaders: + description: |- + AllowHeaders indicates which HTTP request headers are supported for + accessing the requested resource. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Allow-Headers` + response header are separated by a comma (","). + + When the `AllowHeaders` field is configured with one or more headers, the + gateway must return the `Access-Control-Allow-Headers` response header + which value is present in the `AllowHeaders` field. + + If any header name in the `Access-Control-Request-Headers` request header + is not included in the list of header names specified by the response + header `Access-Control-Allow-Headers`, it will present an error on the + client side. + + If any header name in the `Access-Control-Allow-Headers` response header + does not recognize by the client, it will also occur an error on the + client side. + + A wildcard indicates that the requests with all HTTP headers are allowed. + If config contains the wildcard "*" in allowHeaders and the request is + not credentialed, the `Access-Control-Allow-Headers` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Headers from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Headers` response header. When + also the `AllowCredentials` field is true and `AllowHeaders` field + is specified with the `*` wildcard, the gateway must specify one or more + HTTP headers in the value of the `Access-Control-Allow-Headers` response + header. The value of the header `Access-Control-Allow-Headers` is same as + the `Access-Control-Request-Headers` header provided by the client. If + the header `Access-Control-Request-Headers` is not included in the + request, the gateway will omit the `Access-Control-Allow-Headers` + response header, instead of specifying the `*` wildcard. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowHeaders cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowMethods: + description: |- + AllowMethods indicates which HTTP methods are supported for accessing the + requested resource. + + Valid values are any method defined by RFC9110, along with the special + value `*`, which represents all HTTP methods are allowed. + + Method names are case-sensitive, so these values are also case-sensitive. + (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) + + Multiple method names in the value of the `Access-Control-Allow-Methods` + response header are separated by a comma (","). + + A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. + (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The + CORS-safelisted methods are always allowed, regardless of whether they + are specified in the `AllowMethods` field. + + When the `AllowMethods` field is configured with one or more methods, the + gateway must return the `Access-Control-Allow-Methods` response header + which value is present in the `AllowMethods` field. + + If the HTTP method of the `Access-Control-Request-Method` request header + is not included in the list of methods specified by the response header + `Access-Control-Allow-Methods`, it will present an error on the client + side. + + If config contains the wildcard "*" in allowMethods and the request is + not credentialed, the `Access-Control-Allow-Methods` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Method from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Methods` response header. When + also the `AllowCredentials` field is true and `AllowMethods` field + specified with the `*` wildcard, the gateway must specify one HTTP method + in the value of the Access-Control-Allow-Methods response header. The + value of the header `Access-Control-Allow-Methods` is same as the + `Access-Control-Request-Method` header provided by the client. If the + header `Access-Control-Request-Method` is not included in the request, + the gateway will omit the `Access-Control-Allow-Methods` response header, + instead of specifying the `*` wildcard. + + Support: Extended + items: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + - '*' + type: string + maxItems: 9 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowMethods cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowOrigins: + description: |- + AllowOrigins indicates whether the response can be shared with requested + resource from the given `Origin`. + + The `Origin` consists of a scheme and a host, with an optional port, and + takes the form `<scheme>://<host>(:<port>)`. + + Valid values for scheme are: `http` and `https`. + + Valid values for port are any integer between 1 and 65535 (the list of + available TCP/UDP ports). Note that, if not included, port `80` is + assumed for `http` scheme origins, and port `443` is assumed for `https` + origins. This may affect origin matching. + + The host part of the origin may contain the wildcard character `*`. These + wildcard characters behave as follows: + + * `*` is a greedy match to the _left_, including any number of + DNS labels to the left of its position. This also means that + `*` will include any number of period `.` characters to the + left of its position. + * A wildcard by itself matches all hosts. + + An origin value that includes _only_ the `*` character indicates requests + from all `Origin`s are allowed. + + When the `AllowOrigins` field is configured with multiple origins, it + means the server supports clients from multiple origins. If the request + `Origin` matches the configured allowed origins, the gateway must return + the given `Origin` and sets value of the header + `Access-Control-Allow-Origin` same as the `Origin` header provided by the + client. + + The status code of a successful response to a "preflight" request is + always an OK status (i.e., 204 or 200). + + If the request `Origin` does not match the configured allowed origins, + the gateway returns 204/200 response but doesn't set the relevant + cross-origin response headers. Alternatively, the gateway responds with + 403 status to the "preflight" request is denied, coupled with omitting + the CORS headers. The cross-origin request fails on the client side. + Therefore, the client doesn't attempt the actual cross-origin request. + + Conversely, if the request `Origin` matches one of the configured + allowed origins, the gateway sets the response header + `Access-Control-Allow-Origin` to the same value as the `Origin` + header provided by the client. + + When config has the wildcard ("*") in allowOrigins, and the request + is not credentialed (e.g., it is a preflight request), the + `Access-Control-Allow-Origin` response header either contains the + wildcard as well or the Origin from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Origin` response header. When + also the `AllowCredentials` field is true and `AllowOrigins` field + specified with the `*` wildcard, the gateway must return a single origin + in the value of the `Access-Control-Allow-Origin` response header, + instead of specifying the `*` wildcard. The value of the header + `Access-Control-Allow-Origin` is same as the `Origin` header provided by + the client. + + Support: Extended + items: + description: |- + The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and + encoding rules specified in RFC3986. The CORSOrigin MUST include both a + scheme ("http" or "https") and a scheme-specific-part, or it should be a single '*' character. + URIs that include an authority MUST include a fully qualified domain name or + IP address as the host. + maxLength: 253 + minLength: 1 + pattern: (^\*$)|(^(http(s)?):\/\/(((\*\.)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9-]+|\*)(:([0-9]{1,5}))?)$) + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowOrigins cannot contain '*' alongside + other origins + rule: '!(''*'' in self && self.size() > 1)' + exposeHeaders: + description: |- + ExposeHeaders indicates which HTTP response headers can be exposed + to client-side scripts in response to a cross-origin request. + + A CORS-safelisted response header is an HTTP header in a CORS response + that it is considered safe to expose to the client scripts. + The CORS-safelisted response headers include the following headers: + `Cache-Control` + `Content-Language` + `Content-Length` + `Content-Type` + `Expires` + `Last-Modified` + `Pragma` + (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) + The CORS-safelisted response headers are exposed to client by default. + + When an HTTP header name is specified using the `ExposeHeaders` field, + this additional header will be exposed as part of the response to the + client. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Expose-Headers` + response header are separated by a comma (","). + + A wildcard indicates that the responses with all HTTP headers are exposed + to clients. The `Access-Control-Expose-Headers` response header can only + use `*` wildcard as value when the request is not credentialed. + + When the `exposeHeaders` config field contains the "*" wildcard and + the request is credentialed, the gateway cannot use the `*` wildcard in + the `Access-Control-Expose-Headers` response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + maxAge: + default: 5 + description: |- + MaxAge indicates the duration (in seconds) for the client to cache the + results of a "preflight" request. + + The information provided by the `Access-Control-Allow-Methods` and + `Access-Control-Allow-Headers` response headers can be cached by the + client until the time specified by `Access-Control-Max-Age` elapses. + + The default value of `Access-Control-Max-Age` response header is 5 + (seconds). + + When the `MaxAge` field is unspecified, the gateway sets the response + header "Access-Control-Max-Age: 5" by default. + format: int32 + minimum: 1 + type: integer + type: object + extensionRef: + description: |- + ExtensionRef is an optional, implementation-specific extension to the + "filter" behavior. For example, resource "myroutefilter" in group + "networking.example.net"). ExtensionRef MUST NOT be used for core and + extended filters. + + This filter can be used multiple times within the same rule. + + Support: Implementation-specific + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: |- + RequestHeaderModifier defines a schema for a filter that modifies request + headers. + + Support: Core + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: |- + RequestMirror defines a schema for a filter that mirrors requests. + Requests are sent to the specified destination, but responses from + that destination are ignored. + + This filter can be used multiple times within the same rule. Note that + not all implementations will be able to support mirroring to multiple + backends. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef references a resource where mirrored requests are sent. + + Mirrored requests must be sent only to a single destination endpoint + within this BackendRef, irrespective of how many endpoints are present + within this BackendRef. + + If the referent cannot be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure the "ResolvedRefs" + condition on the Route status is set to `status: False` and not configure + this backend in the underlying implementation. + + If there is a cross-namespace reference to an *existing* object + that is not allowed by a ReferenceGrant, the controller must ensure the + "ResolvedRefs" condition on the Route is set to `status: False`, + with the "RefNotPermitted" reason and not configure this backend in the + underlying implementation. + + In either error case, the Message of the `ResolvedRefs` Condition + should be used to provide more detail about the problem. + + Support: Extended for Kubernetes Service + + Support: Implementation-specific for any other resource + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + fraction: + description: |- + Fraction represents the fraction of requests that should be + mirrored to BackendRef. + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to + denominator + rule: self.numerator <= self.denominator + percent: + description: |- + Percent represents the percentage of requests that should be + mirrored to BackendRef. Its minimum value is 0 (indicating 0% of + requests) and its maximum value is 100 (indicating 100% of requests). + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + format: int32 + maximum: 100 + minimum: 0 + type: integer + required: + - backendRef + type: object + x-kubernetes-validations: + - message: Only one of percent or fraction may be specified + in HTTPRequestMirrorFilter + rule: '!(has(self.percent) && has(self.fraction))' + requestRedirect: + description: |- + RequestRedirect defines a schema for a filter that responds to the + request with an HTTP redirection. + + Support: Core + properties: + hostname: + description: |- + Hostname is the hostname to be used in the value of the `Location` + header in the response. + When empty, the hostname in the `Host` header of the request is used. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines parameters used to modify the path of the incoming request. + The modified path is then used to construct the `Location` header. When + empty, the request path is used as-is. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + port: + description: |- + Port is the port to be used in the value of the `Location` + header in the response. + + If no port is specified, the redirect port MUST be derived using the + following rules: + + * If redirect scheme is not-empty, the redirect port MUST be the well-known + port associated with the redirect scheme. Specifically "http" to port 80 + and "https" to port 443. If the redirect scheme does not have a + well-known port, the listener port of the Gateway SHOULD be used. + * If redirect scheme is empty, the redirect port MUST be the Gateway + Listener port. + + Implementations SHOULD NOT add the port number in the 'Location' + header in the following cases: + + * A Location header that will use HTTP (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 80. + * A Location header that will use HTTPS (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 443. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: |- + Scheme is the scheme to be used in the value of the `Location` header in + the response. When empty, the scheme of the request is used. + + Scheme redirects can affect the port of the redirect, for more information, + refer to the documentation for the port field of this filter. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Extended + enum: + - http + - https + type: string + statusCode: + default: 302 + description: |- + StatusCode is the HTTP status code to be used in response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Core + enum: + - 301 + - 302 + - 303 + - 307 + - 308 + type: integer + type: object + responseHeaderModifier: + description: |- + ResponseHeaderModifier defines a schema for a filter that modifies response + headers. + + Support: Extended + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: |- + Type identifies the type of filter to apply. As with other API fields, + types are classified into three conformance levels: + + - Core: Filter types and their corresponding configuration defined by + "Support: Core" in this package, e.g. "RequestHeaderModifier". All + implementations must support core filters. + + - Extended: Filter types and their corresponding configuration defined by + "Support: Extended" in this package, e.g. "RequestMirror". Implementers + are encouraged to support extended filters. + + - Implementation-specific: Filters that are defined and supported by + specific vendors. + In the future, filters showing convergence in behavior across multiple + implementations will be considered for inclusion in extended or core + conformance levels. Filter-specific configuration for such filters + is specified using the ExtensionRef field. `Type` should be set to + "ExtensionRef" for custom filters. + + Implementers are encouraged to define custom implementation types to + extend the core API with implementation-specific behavior. + + If a reference to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have been processed by + that filter MUST receive a HTTP error response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + - CORS + type: string + urlRewrite: + description: |- + URLRewrite defines a schema for a filter that modifies a request during forwarding. + + Support: Extended + properties: + hostname: + description: |- + Hostname is the value to be used to replace the Host header value during + forwarding. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines a path rewrite. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + type: object + required: + - type + type: object + x-kubernetes-validations: + - message: filter.cors must be nil if the filter.type is not + CORS + rule: '!(has(self.cors) && self.type != ''CORS'')' + - message: filter.cors must be specified for CORS filter.type + rule: '!(!has(self.cors) && self.type == ''CORS'')' + - message: filter.requestHeaderModifier must be nil if the + filter.type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type != + ''RequestHeaderModifier'')' + - message: filter.requestHeaderModifier must be specified + for RequestHeaderModifier filter.type + rule: '!(!has(self.requestHeaderModifier) && self.type == + ''RequestHeaderModifier'')' + - message: filter.responseHeaderModifier must be nil if the + filter.type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type != + ''ResponseHeaderModifier'')' + - message: filter.responseHeaderModifier must be specified + for ResponseHeaderModifier filter.type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.requestMirror must be nil if the filter.type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.requestMirror must be specified for RequestMirror + filter.type + rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')' + - message: filter.requestRedirect must be nil if the filter.type + is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != ''RequestRedirect'')' + - message: filter.requestRedirect must be specified for RequestRedirect + filter.type + rule: '!(!has(self.requestRedirect) && self.type == ''RequestRedirect'')' + - message: filter.urlRewrite must be nil if the filter.type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.urlRewrite must be specified for URLRewrite + filter.type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.extensionRef must be nil if the filter.type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.extensionRef must be specified for ExtensionRef + filter.type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') && + self.exists(f, f.type == ''URLRewrite''))' + - message: RequestHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'RequestHeaderModifier').size() + <= 1 + - message: ResponseHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'ResponseHeaderModifier').size() + <= 1 + - message: RequestRedirect filter cannot be repeated + rule: self.filter(f, f.type == 'RequestRedirect').size() <= + 1 + - message: URLRewrite filter cannot be repeated + rule: self.filter(f, f.type == 'URLRewrite').size() <= 1 + matches: + default: + - path: + type: PathPrefix + value: / + description: |- + Matches define conditions used for matching the rule against incoming + HTTP requests. Each match is independent, i.e. this rule will be matched + if **any** one of the matches is satisfied. + + For example, take the following matches configuration: + + ``` + matches: + - path: + value: "/foo" + headers: + - name: "version" + value: "v2" + - path: + value: "/v2/foo" + ``` + + For a request to match against this rule, a request must satisfy + EITHER of the two conditions: + + - path prefixed with `/foo` AND contains the header `version: v2` + - path prefix of `/v2/foo` + + See the documentation for HTTPRouteMatch on how to specify multiple + match conditions that should be ANDed together. + + If no matches are specified, the default is a prefix + path match on "/", which has the effect of matching every + HTTP request. + + Proxy or Load Balancer routing configuration generated from HTTPRoutes + MUST prioritize matches based on the following criteria, continuing on + ties. Across all rules specified on applicable Routes, precedence must be + given to the match having: + + * "Exact" path match. + * "Prefix" path match with largest number of characters. + * Method match. + * Largest number of header matches. + * Largest number of query param matches. + + Note: The precedence of RegularExpression path matches are implementation-specific. + + If ties still exist across multiple Routes, matching precedence MUST be + determined in order of the following criteria, continuing on ties: + + * The oldest Route based on creation timestamp. + * The Route appearing first in alphabetical order by + "{namespace}/{name}". + + If ties still exist within an HTTPRoute, matching precedence MUST be granted + to the FIRST matching rule (in list order) with a match meeting the above + criteria. + + When no rules matching a request have been successfully attached to the + parent a request is coming from, a HTTP 404 status code MUST be returned. + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given\naction. Multiple match types + are ANDed together, i.e. the match will\nevaluate to true + only if all conditions are satisfied.\n\nFor example, the + match below will match a HTTP request only if its path\nstarts + with `/foo` AND it contains the `version: v1` header:\n\n```\nmatch:\n\n\tpath:\n\t + \ value: \"/foo\"\n\theaders:\n\t- name: \"version\"\n\t + \ value \"v1\"\n\n```" + properties: + headers: + description: |- + Headers specifies HTTP request header matchers. Multiple match values are + ANDed together, meaning, a request must match all the specified headers + to select the route. + items: + description: |- + HTTPHeaderMatch describes how to select a HTTP route by matching HTTP request + headers. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, only the first + entry with an equivalent name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + + When a header is repeated in an HTTP request, it is + implementation-specific behavior as to how this is represented. + Generally, proxies should follow the guidance from the RFC: + https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 regarding + processing a repeated header, with special handling for "Set-Cookie". + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: |- + Type specifies how to match against the value of the header. + + Support: Core (Exact) + + Support: Implementation-specific (RegularExpression) + + Since RegularExpression HeaderMatchType has implementation-specific + conformance, implementations can support POSIX, PCRE or any other dialects + of regular expressions. Please read the implementation's documentation to + determine the supported dialect. + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: |- + Method specifies HTTP method matcher. + When specified, this route will be matched only if the request has the + specified method. + + Support: Extended + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: |- + Path specifies a HTTP request path matcher. If this field is not + specified, a default prefix match on the "/" path is provided. + properties: + type: + default: PathPrefix + description: |- + Type specifies how to match against the path Value. + + Support: Core (Exact, PathPrefix) + + Support: Implementation-specific (RegularExpression) + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + x-kubernetes-validations: + - message: value must be an absolute path and start with + '/' when type one of ['Exact', 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? self.value.startsWith(''/'') + : true' + - message: must not contain '//' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''//'') + : true' + - message: must not contain '/./' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''/./'') + : true' + - message: must not contain '/../' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''/../'') + : true' + - message: must not contain '%2f' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''%2f'') + : true' + - message: must not contain '%2F' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''%2F'') + : true' + - message: must not contain '#' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''#'') + : true' + - message: must not end with '/..' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.endsWith(''/..'') + : true' + - message: must not end with '/.' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.endsWith(''/.'') + : true' + - message: type must be one of ['Exact', 'PathPrefix', + 'RegularExpression'] + rule: self.type in ['Exact','PathPrefix'] || self.type + == 'RegularExpression' + - message: must only contain valid characters (matching + ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$) + for types ['Exact', 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? self.value.matches(r"""^(?:[-A-Za-z0-9/._~!$&''()*+,;=:@]|[%][0-9a-fA-F]{2})+$""") + : true' + queryParams: + description: |- + QueryParams specifies HTTP query parameter matchers. Multiple match + values are ANDed together, meaning, a request must match all the + specified query parameters to select the route. + + Support: Extended + items: + description: |- + HTTPQueryParamMatch describes how to select a HTTP route by matching HTTP + query parameters. + properties: + name: + description: |- + Name is the name of the HTTP query param to be matched. This must be an + exact string match. (See + https://tools.ietf.org/html/rfc7230#section-2.7.3). + + If multiple entries specify equivalent query param names, only the first + entry with an equivalent name MUST be considered for a match. Subsequent + entries with an equivalent query param name MUST be ignored. + + If a query param is repeated in an HTTP request, the behavior is + purposely left undefined, since different data planes have different + capabilities. However, it is *recommended* that implementations should + match against the first value of the param if the data plane supports it, + as this behavior is expected in other load balancing contexts outside of + the Gateway API. + + Users SHOULD NOT route traffic based on repeated query params to guard + themselves against potential differences in the implementations. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: |- + Type specifies how to match against the value of the query parameter. + + Support: Extended (Exact) + + Support: Implementation-specific (RegularExpression) + + Since RegularExpression QueryParamMatchType has Implementation-specific + conformance, implementations can support POSIX, PCRE or any other + dialects of regular expressions. Please read the implementation's + documentation to determine the supported dialect. + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + name: + description: |- + Name is the name of the route rule. This name MUST be unique within a Route if it is set. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + timeouts: + description: |- + Timeouts defines the timeouts that can be configured for an HTTP request. + + Support: Extended + properties: + backendRequest: + description: |- + BackendRequest specifies a timeout for an individual request from the gateway + to a backend. This covers the time from when the request first starts being + sent from the gateway to when the full response has been received from the backend. + + Setting a timeout to the zero duration (e.g. "0s") SHOULD disable the timeout + completely. Implementations that cannot completely disable the timeout MUST + instead interpret the zero duration as the longest possible value to which + the timeout can be set. + + An entire client HTTP transaction with a gateway, covered by the Request timeout, + may result in more than one call from the gateway to the destination backend, + for example, if automatic retries are supported. + + The value of BackendRequest must be a Gateway API Duration string as defined by + GEP-2257. When this field is unspecified, its behavior is implementation-specific; + when specified, the value of BackendRequest must be no more than the value of the + Request timeout (since the Request timeout encompasses the BackendRequest timeout). + + Support: Extended + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + request: + description: |- + Request specifies the maximum duration for a gateway to respond to an HTTP request. + If the gateway has not been able to respond before this deadline is met, the gateway + MUST return a timeout error. + + For example, setting the `rules.timeouts.request` field to the value `10s` in an + `HTTPRoute` will cause a timeout if a client request is taking longer than 10 seconds + to complete. + + Setting a timeout to the zero duration (e.g. "0s") SHOULD disable the timeout + completely. Implementations that cannot completely disable the timeout MUST + instead interpret the zero duration as the longest possible value to which + the timeout can be set. + + This timeout is intended to cover as close to the whole request-response transaction + as possible although an implementation MAY choose to start the timeout after the entire + request stream has been received instead of immediately after the transaction is + initiated by the client. + + The value of Request is a Gateway API Duration string as defined by GEP-2257. When this + field is unspecified, request timeout behavior is implementation-specific. + + Support: Extended + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + x-kubernetes-validations: + - message: backendRequest timeout cannot be longer than request + timeout + rule: '!(has(self.request) && has(self.backendRequest) && + duration(self.request) != duration(''0s'') && duration(self.backendRequest) + > duration(self.request))' + type: object + x-kubernetes-validations: + - message: RequestRedirect filter must not be used together with + backendRefs + rule: '(has(self.backendRefs) && size(self.backendRefs) > 0) ? + (!has(self.filters) || self.filters.all(f, !has(f.requestRedirect))): + true' + - message: When using RequestRedirect filter with path.replacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + ? ((size(self.matches) != 1 || !has(self.matches[0].path) || + self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: When using URLRewrite filter with path.replacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' + - message: Within backendRefs, when using RequestRedirect filter + with path.replacePrefixMatch, exactly one PathPrefix match must + be specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + )) ? ((size(self.matches) != 1 || !has(self.matches[0].path) + || self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: Within backendRefs, When using URLRewrite filter with + path.replacePrefixMatch, exactly one PathPrefix match must be + specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) )) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: While 16 rules and 64 matches per rule are allowed, the + total number of matches across all rules in a route must be less + than 128 + rule: '(self.size() > 0 ? self[0].matches.size() : 0) + (self.size() + > 1 ? self[1].matches.size() : 0) + (self.size() > 2 ? self[2].matches.size() + : 0) + (self.size() > 3 ? self[3].matches.size() : 0) + (self.size() + > 4 ? self[4].matches.size() : 0) + (self.size() > 5 ? self[5].matches.size() + : 0) + (self.size() > 6 ? self[6].matches.size() : 0) + (self.size() + > 7 ? self[7].matches.size() : 0) + (self.size() > 8 ? self[8].matches.size() + : 0) + (self.size() > 9 ? self[9].matches.size() : 0) + (self.size() + > 10 ? self[10].matches.size() : 0) + (self.size() > 11 ? self[11].matches.size() + : 0) + (self.size() > 12 ? self[12].matches.size() : 0) + (self.size() + > 13 ? self[13].matches.size() : 0) + (self.size() > 14 ? self[14].matches.size() + : 0) + (self.size() > 15 ? self[15].matches.size() : 0) <= 128' + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.hostnames + name: Hostnames + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + HTTPRoute provides a way to route HTTP requests. This includes the capability + to match requests by hostname, path, header, or query param. Filters can be + used to specify additional processing steps. Backends specify where matching + requests should be routed. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of HTTPRoute. + properties: + hostnames: + description: |- + Hostnames defines a set of hostnames that should match against the HTTP Host + header to select a HTTPRoute used to process the request. Implementations + MUST ignore any port value specified in the HTTP Host header while + performing a match and (absent of any applicable header modification + configuration) MUST forward this header unmodified to the backend. + + Valid values for Hostnames are determined by RFC 1123 definition of a + hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + If a hostname is specified by both the Listener and HTTPRoute, there + must be at least one intersecting hostname for the HTTPRoute to be + attached to the Listener. For example: + + * A Listener with `test.example.com` as the hostname matches HTTPRoutes + that have either not specified any hostnames, or have specified at + least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches HTTPRoutes + that have either not specified any hostnames or have specified at least + one hostname that matches the Listener hostname. For example, + `*.example.com`, `test.example.com`, and `foo.test.example.com` would + all match. On the other hand, `example.com` and `test.example.net` would + not match. + + Hostnames that are prefixed with a wildcard label (`*.`) are interpreted + as a suffix match. That means that a match for `*.example.com` would match + both `test.example.com`, and `foo.test.example.com`, but not `example.com`. + + If both the Listener and HTTPRoute have specified hostnames, any + HTTPRoute hostnames that do not match the Listener hostname MUST be + ignored. For example, if a Listener specified `*.example.com`, and the + HTTPRoute specified `test.example.com` and `test.example.net`, + `test.example.net` must not be considered for a match. + + If both the Listener and HTTPRoute have specified hostnames, and none + match with the criteria above, then the HTTPRoute is not accepted. The + implementation must raise an 'Accepted' Condition with a status of + `False` in the corresponding RouteParentStatus. + + In the event that multiple HTTPRoutes specify intersecting hostnames (e.g. + overlapping wildcard matching and exact matching hostnames), precedence must + be given to rules from the HTTPRoute with the largest number of: + + * Characters in a matching non-wildcard hostname. + * Characters in a matching hostname. + + If ties exist across multiple Routes, the matching precedence rules for + HTTPRouteMatches takes over. + + Support: Core + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + default: + - matches: + - path: + type: PathPrefix + value: / + description: Rules are a list of HTTP matchers, filters and actions. + items: + description: |- + HTTPRouteRule defines semantics for matching an HTTP request based on + conditions (matches), processing it (filters), and forwarding the request to + an API object (backendRefs). + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. + + Failure behavior here depends on how many BackendRefs are specified and + how many are invalid. + + If *all* entries in BackendRefs are invalid, and there are also no filters + specified in this route rule, *all* traffic which matches this rule MUST + receive a 500 status code. + + See the HTTPBackendRef definition for the rules about what makes a single + HTTPBackendRef invalid. + + When a HTTPBackendRef is invalid, 500 status codes MUST be returned for + requests that would have otherwise been routed to an invalid backend. If + multiple backends are specified, and some are invalid, the proportion of + requests that would otherwise have been routed to an invalid backend + MUST receive a 500 status code. + + For example, if two backends are specified with equal weights, and one is + invalid, 50 percent of traffic must receive a 500. Implementations may + choose how that 50 percent is determined. + + When a HTTPBackendRef refers to a Service that has no ready endpoints, + implementations SHOULD return a 503 for requests to that backend instead. + If an implementation chooses to do this, all of the above rules for 500 responses + MUST also apply for responses that return a 503. + + Support: Core for Kubernetes Service + + Support: Extended for Kubernetes ServiceImport + + Support: Implementation-specific for any other resource + + Support for weight: Core + items: + description: |- + HTTPBackendRef defines how a HTTPRoute forwards a HTTP request. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + properties: + filters: + description: |- + Filters defined at this level should be executed if and only if the + request is being forwarded to the backend defined here. + + Support: Implementation-specific (For broader support of filters, use the + Filters field in HTTPRouteRule.) + items: + description: |- + HTTPRouteFilter defines processing steps that must be completed during the + request or response lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway implementations. Some + examples include request or response modification, implementing + authentication strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type of the filter. + properties: + cors: + description: |- + CORS defines a schema for a filter that responds to the + cross-origin request based on HTTP response header. + + Support: Extended + properties: + allowCredentials: + description: |- + AllowCredentials indicates whether the actual cross-origin request allows + to include credentials. + + When set to true, the gateway will include the `Access-Control-Allow-Credentials` + response header with value true (case-sensitive). + + When set to false or omitted the gateway will omit the header + `Access-Control-Allow-Credentials` entirely (this is the standard CORS + behavior). + + Support: Extended + type: boolean + allowHeaders: + description: |- + AllowHeaders indicates which HTTP request headers are supported for + accessing the requested resource. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Allow-Headers` + response header are separated by a comma (","). + + When the `AllowHeaders` field is configured with one or more headers, the + gateway must return the `Access-Control-Allow-Headers` response header + which value is present in the `AllowHeaders` field. + + If any header name in the `Access-Control-Request-Headers` request header + is not included in the list of header names specified by the response + header `Access-Control-Allow-Headers`, it will present an error on the + client side. + + If any header name in the `Access-Control-Allow-Headers` response header + does not recognize by the client, it will also occur an error on the + client side. + + A wildcard indicates that the requests with all HTTP headers are allowed. + If config contains the wildcard "*" in allowHeaders and the request is + not credentialed, the `Access-Control-Allow-Headers` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Headers from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Headers` response header. When + also the `AllowCredentials` field is true and `AllowHeaders` field + is specified with the `*` wildcard, the gateway must specify one or more + HTTP headers in the value of the `Access-Control-Allow-Headers` response + header. The value of the header `Access-Control-Allow-Headers` is same as + the `Access-Control-Request-Headers` header provided by the client. If + the header `Access-Control-Request-Headers` is not included in the + request, the gateway will omit the `Access-Control-Allow-Headers` + response header, instead of specifying the `*` wildcard. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowHeaders cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowMethods: + description: |- + AllowMethods indicates which HTTP methods are supported for accessing the + requested resource. + + Valid values are any method defined by RFC9110, along with the special + value `*`, which represents all HTTP methods are allowed. + + Method names are case-sensitive, so these values are also case-sensitive. + (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) + + Multiple method names in the value of the `Access-Control-Allow-Methods` + response header are separated by a comma (","). + + A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. + (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The + CORS-safelisted methods are always allowed, regardless of whether they + are specified in the `AllowMethods` field. + + When the `AllowMethods` field is configured with one or more methods, the + gateway must return the `Access-Control-Allow-Methods` response header + which value is present in the `AllowMethods` field. + + If the HTTP method of the `Access-Control-Request-Method` request header + is not included in the list of methods specified by the response header + `Access-Control-Allow-Methods`, it will present an error on the client + side. + + If config contains the wildcard "*" in allowMethods and the request is + not credentialed, the `Access-Control-Allow-Methods` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Method from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Methods` response header. When + also the `AllowCredentials` field is true and `AllowMethods` field + specified with the `*` wildcard, the gateway must specify one HTTP method + in the value of the Access-Control-Allow-Methods response header. The + value of the header `Access-Control-Allow-Methods` is same as the + `Access-Control-Request-Method` header provided by the client. If the + header `Access-Control-Request-Method` is not included in the request, + the gateway will omit the `Access-Control-Allow-Methods` response header, + instead of specifying the `*` wildcard. + + Support: Extended + items: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + - '*' + type: string + maxItems: 9 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowMethods cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowOrigins: + description: |- + AllowOrigins indicates whether the response can be shared with requested + resource from the given `Origin`. + + The `Origin` consists of a scheme and a host, with an optional port, and + takes the form `<scheme>://<host>(:<port>)`. + + Valid values for scheme are: `http` and `https`. + + Valid values for port are any integer between 1 and 65535 (the list of + available TCP/UDP ports). Note that, if not included, port `80` is + assumed for `http` scheme origins, and port `443` is assumed for `https` + origins. This may affect origin matching. + + The host part of the origin may contain the wildcard character `*`. These + wildcard characters behave as follows: + + * `*` is a greedy match to the _left_, including any number of + DNS labels to the left of its position. This also means that + `*` will include any number of period `.` characters to the + left of its position. + * A wildcard by itself matches all hosts. + + An origin value that includes _only_ the `*` character indicates requests + from all `Origin`s are allowed. + + When the `AllowOrigins` field is configured with multiple origins, it + means the server supports clients from multiple origins. If the request + `Origin` matches the configured allowed origins, the gateway must return + the given `Origin` and sets value of the header + `Access-Control-Allow-Origin` same as the `Origin` header provided by the + client. + + The status code of a successful response to a "preflight" request is + always an OK status (i.e., 204 or 200). + + If the request `Origin` does not match the configured allowed origins, + the gateway returns 204/200 response but doesn't set the relevant + cross-origin response headers. Alternatively, the gateway responds with + 403 status to the "preflight" request is denied, coupled with omitting + the CORS headers. The cross-origin request fails on the client side. + Therefore, the client doesn't attempt the actual cross-origin request. + + Conversely, if the request `Origin` matches one of the configured + allowed origins, the gateway sets the response header + `Access-Control-Allow-Origin` to the same value as the `Origin` + header provided by the client. + + When config has the wildcard ("*") in allowOrigins, and the request + is not credentialed (e.g., it is a preflight request), the + `Access-Control-Allow-Origin` response header either contains the + wildcard as well or the Origin from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Origin` response header. When + also the `AllowCredentials` field is true and `AllowOrigins` field + specified with the `*` wildcard, the gateway must return a single origin + in the value of the `Access-Control-Allow-Origin` response header, + instead of specifying the `*` wildcard. The value of the header + `Access-Control-Allow-Origin` is same as the `Origin` header provided by + the client. + + Support: Extended + items: + description: |- + The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and + encoding rules specified in RFC3986. The CORSOrigin MUST include both a + scheme ("http" or "https") and a scheme-specific-part, or it should be a single '*' character. + URIs that include an authority MUST include a fully qualified domain name or + IP address as the host. + maxLength: 253 + minLength: 1 + pattern: (^\*$)|(^(http(s)?):\/\/(((\*\.)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9-]+|\*)(:([0-9]{1,5}))?)$) + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowOrigins cannot contain '*' alongside + other origins + rule: '!(''*'' in self && self.size() > 1)' + exposeHeaders: + description: |- + ExposeHeaders indicates which HTTP response headers can be exposed + to client-side scripts in response to a cross-origin request. + + A CORS-safelisted response header is an HTTP header in a CORS response + that it is considered safe to expose to the client scripts. + The CORS-safelisted response headers include the following headers: + `Cache-Control` + `Content-Language` + `Content-Length` + `Content-Type` + `Expires` + `Last-Modified` + `Pragma` + (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) + The CORS-safelisted response headers are exposed to client by default. + + When an HTTP header name is specified using the `ExposeHeaders` field, + this additional header will be exposed as part of the response to the + client. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Expose-Headers` + response header are separated by a comma (","). + + A wildcard indicates that the responses with all HTTP headers are exposed + to clients. The `Access-Control-Expose-Headers` response header can only + use `*` wildcard as value when the request is not credentialed. + + When the `exposeHeaders` config field contains the "*" wildcard and + the request is credentialed, the gateway cannot use the `*` wildcard in + the `Access-Control-Expose-Headers` response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + maxAge: + default: 5 + description: |- + MaxAge indicates the duration (in seconds) for the client to cache the + results of a "preflight" request. + + The information provided by the `Access-Control-Allow-Methods` and + `Access-Control-Allow-Headers` response headers can be cached by the + client until the time specified by `Access-Control-Max-Age` elapses. + + The default value of `Access-Control-Max-Age` response header is 5 + (seconds). + + When the `MaxAge` field is unspecified, the gateway sets the response + header "Access-Control-Max-Age: 5" by default. + format: int32 + minimum: 1 + type: integer + type: object + extensionRef: + description: |- + ExtensionRef is an optional, implementation-specific extension to the + "filter" behavior. For example, resource "myroutefilter" in group + "networking.example.net"). ExtensionRef MUST NOT be used for core and + extended filters. + + This filter can be used multiple times within the same rule. + + Support: Implementation-specific + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For + example "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: |- + RequestHeaderModifier defines a schema for a filter that modifies request + headers. + + Support: Core + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: |- + RequestMirror defines a schema for a filter that mirrors requests. + Requests are sent to the specified destination, but responses from + that destination are ignored. + + This filter can be used multiple times within the same rule. Note that + not all implementations will be able to support mirroring to multiple + backends. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef references a resource where mirrored requests are sent. + + Mirrored requests must be sent only to a single destination endpoint + within this BackendRef, irrespective of how many endpoints are present + within this BackendRef. + + If the referent cannot be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure the "ResolvedRefs" + condition on the Route status is set to `status: False` and not configure + this backend in the underlying implementation. + + If there is a cross-namespace reference to an *existing* object + that is not allowed by a ReferenceGrant, the controller must ensure the + "ResolvedRefs" condition on the Route is set to `status: False`, + with the "RefNotPermitted" reason and not configure this backend in the + underlying implementation. + + In either error case, the Message of the `ResolvedRefs` Condition + should be used to provide more detail about the problem. + + Support: Extended for Kubernetes Service + + Support: Implementation-specific for any other resource + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind + == ''Service'') ? has(self.port) : true' + fraction: + description: |- + Fraction represents the fraction of requests that should be + mirrored to BackendRef. + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal + to denominator + rule: self.numerator <= self.denominator + percent: + description: |- + Percent represents the percentage of requests that should be + mirrored to BackendRef. Its minimum value is 0 (indicating 0% of + requests) and its maximum value is 100 (indicating 100% of requests). + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + format: int32 + maximum: 100 + minimum: 0 + type: integer + required: + - backendRef + type: object + x-kubernetes-validations: + - message: Only one of percent or fraction may be + specified in HTTPRequestMirrorFilter + rule: '!(has(self.percent) && has(self.fraction))' + requestRedirect: + description: |- + RequestRedirect defines a schema for a filter that responds to the + request with an HTTP redirection. + + Support: Core + properties: + hostname: + description: |- + Hostname is the hostname to be used in the value of the `Location` + header in the response. + When empty, the hostname in the `Host` header of the request is used. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines parameters used to modify the path of the incoming request. + The modified path is then used to construct the `Location` header. When + empty, the request path is used as-is. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified + when type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified + when type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + port: + description: |- + Port is the port to be used in the value of the `Location` + header in the response. + + If no port is specified, the redirect port MUST be derived using the + following rules: + + * If redirect scheme is not-empty, the redirect port MUST be the well-known + port associated with the redirect scheme. Specifically "http" to port 80 + and "https" to port 443. If the redirect scheme does not have a + well-known port, the listener port of the Gateway SHOULD be used. + * If redirect scheme is empty, the redirect port MUST be the Gateway + Listener port. + + Implementations SHOULD NOT add the port number in the 'Location' + header in the following cases: + + * A Location header that will use HTTP (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 80. + * A Location header that will use HTTPS (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 443. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: |- + Scheme is the scheme to be used in the value of the `Location` header in + the response. When empty, the scheme of the request is used. + + Scheme redirects can affect the port of the redirect, for more information, + refer to the documentation for the port field of this filter. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Extended + enum: + - http + - https + type: string + statusCode: + default: 302 + description: |- + StatusCode is the HTTP status code to be used in response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Core + enum: + - 301 + - 302 + - 303 + - 307 + - 308 + type: integer + type: object + responseHeaderModifier: + description: |- + ResponseHeaderModifier defines a schema for a filter that modifies response + headers. + + Support: Extended + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP + Header name and value as defined by RFC + 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: |- + Type identifies the type of filter to apply. As with other API fields, + types are classified into three conformance levels: + + - Core: Filter types and their corresponding configuration defined by + "Support: Core" in this package, e.g. "RequestHeaderModifier". All + implementations must support core filters. + + - Extended: Filter types and their corresponding configuration defined by + "Support: Extended" in this package, e.g. "RequestMirror". Implementers + are encouraged to support extended filters. + + - Implementation-specific: Filters that are defined and supported by + specific vendors. + In the future, filters showing convergence in behavior across multiple + implementations will be considered for inclusion in extended or core + conformance levels. Filter-specific configuration for such filters + is specified using the ExtensionRef field. `Type` should be set to + "ExtensionRef" for custom filters. + + Implementers are encouraged to define custom implementation types to + extend the core API with implementation-specific behavior. + + If a reference to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have been processed by + that filter MUST receive a HTTP error response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + - CORS + type: string + urlRewrite: + description: |- + URLRewrite defines a schema for a filter that modifies a request during forwarding. + + Support: Extended + properties: + hostname: + description: |- + Hostname is the value to be used to replace the Host header value during + forwarding. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines a path rewrite. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified + when type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? + has(self.replaceFullPath) : true' + - message: type must be 'ReplaceFullPath' when + replaceFullPath is set + rule: 'has(self.replaceFullPath) ? self.type + == ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified + when type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' + ? has(self.replacePrefixMatch) : true' + - message: type must be 'ReplacePrefixMatch' + when replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + type: object + required: + - type + type: object + x-kubernetes-validations: + - message: filter.cors must be nil if the filter.type + is not CORS + rule: '!(has(self.cors) && self.type != ''CORS'')' + - message: filter.cors must be specified for CORS filter.type + rule: '!(!has(self.cors) && self.type == ''CORS'')' + - message: filter.requestHeaderModifier must be nil + if the filter.type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type + != ''RequestHeaderModifier'')' + - message: filter.requestHeaderModifier must be specified + for RequestHeaderModifier filter.type + rule: '!(!has(self.requestHeaderModifier) && self.type + == ''RequestHeaderModifier'')' + - message: filter.responseHeaderModifier must be nil + if the filter.type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type + != ''ResponseHeaderModifier'')' + - message: filter.responseHeaderModifier must be specified + for ResponseHeaderModifier filter.type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.requestMirror must be nil if the filter.type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.requestMirror must be specified for + RequestMirror filter.type + rule: '!(!has(self.requestMirror) && self.type == + ''RequestMirror'')' + - message: filter.requestRedirect must be nil if the + filter.type is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != + ''RequestRedirect'')' + - message: filter.requestRedirect must be specified + for RequestRedirect filter.type + rule: '!(!has(self.requestRedirect) && self.type == + ''RequestRedirect'')' + - message: filter.urlRewrite must be nil if the filter.type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.urlRewrite must be specified for URLRewrite + filter.type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.extensionRef must be nil if the filter.type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.extensionRef must be specified for + ExtensionRef filter.type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') + && self.exists(f, f.type == ''URLRewrite''))' + - message: RequestHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'RequestHeaderModifier').size() + <= 1 + - message: ResponseHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'ResponseHeaderModifier').size() + <= 1 + - message: RequestRedirect filter cannot be repeated + rule: self.filter(f, f.type == 'RequestRedirect').size() + <= 1 + - message: URLRewrite filter cannot be repeated + rule: self.filter(f, f.type == 'URLRewrite').size() + <= 1 + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + filters: + description: |- + Filters define the filters that are applied to requests that match + this rule. + + Wherever possible, implementations SHOULD implement filters in the order + they are specified. + + Implementations MAY choose to implement this ordering strictly, rejecting + any combination or order of filters that cannot be supported. If implementations + choose a strict interpretation of filter ordering, they MUST clearly document + that behavior. + + To reject an invalid combination or order of filters, implementations SHOULD + consider the Route Rules with this configuration invalid. If all Route Rules + in a Route are invalid, the entire Route would be considered invalid. If only + a portion of Route Rules are invalid, implementations MUST set the + "PartiallyInvalid" condition for the Route. + + Conformance-levels at this level are defined based on the type of filter: + + - ALL core filters MUST be supported by all implementations. + - Implementers are encouraged to support extended filters. + - Implementation-specific custom filters have no API guarantees across + implementations. + + Specifying the same filter multiple times is not supported unless explicitly + indicated in the filter. + + All filters are expected to be compatible with each other except for the + URLRewrite and RequestRedirect filters, which may not be combined. If an + implementation cannot support other combinations of filters, they must clearly + document that limitation. In cases where incompatible or unsupported + filters are specified and cause the `Accepted` condition to be set to status + `False`, implementations may use the `IncompatibleFilters` reason to specify + this configuration error. + + Support: Core + items: + description: |- + HTTPRouteFilter defines processing steps that must be completed during the + request or response lifecycle. HTTPRouteFilters are meant as an extension + point to express processing that may be done in Gateway implementations. Some + examples include request or response modification, implementing + authentication strategies, rate-limiting, and traffic shaping. API + guarantee/conformance is defined based on the type of the filter. + properties: + cors: + description: |- + CORS defines a schema for a filter that responds to the + cross-origin request based on HTTP response header. + + Support: Extended + properties: + allowCredentials: + description: |- + AllowCredentials indicates whether the actual cross-origin request allows + to include credentials. + + When set to true, the gateway will include the `Access-Control-Allow-Credentials` + response header with value true (case-sensitive). + + When set to false or omitted the gateway will omit the header + `Access-Control-Allow-Credentials` entirely (this is the standard CORS + behavior). + + Support: Extended + type: boolean + allowHeaders: + description: |- + AllowHeaders indicates which HTTP request headers are supported for + accessing the requested resource. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Allow-Headers` + response header are separated by a comma (","). + + When the `AllowHeaders` field is configured with one or more headers, the + gateway must return the `Access-Control-Allow-Headers` response header + which value is present in the `AllowHeaders` field. + + If any header name in the `Access-Control-Request-Headers` request header + is not included in the list of header names specified by the response + header `Access-Control-Allow-Headers`, it will present an error on the + client side. + + If any header name in the `Access-Control-Allow-Headers` response header + does not recognize by the client, it will also occur an error on the + client side. + + A wildcard indicates that the requests with all HTTP headers are allowed. + If config contains the wildcard "*" in allowHeaders and the request is + not credentialed, the `Access-Control-Allow-Headers` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Headers from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Headers` response header. When + also the `AllowCredentials` field is true and `AllowHeaders` field + is specified with the `*` wildcard, the gateway must specify one or more + HTTP headers in the value of the `Access-Control-Allow-Headers` response + header. The value of the header `Access-Control-Allow-Headers` is same as + the `Access-Control-Request-Headers` header provided by the client. If + the header `Access-Control-Request-Headers` is not included in the + request, the gateway will omit the `Access-Control-Allow-Headers` + response header, instead of specifying the `*` wildcard. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowHeaders cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowMethods: + description: |- + AllowMethods indicates which HTTP methods are supported for accessing the + requested resource. + + Valid values are any method defined by RFC9110, along with the special + value `*`, which represents all HTTP methods are allowed. + + Method names are case-sensitive, so these values are also case-sensitive. + (See https://www.rfc-editor.org/rfc/rfc2616#section-5.1.1) + + Multiple method names in the value of the `Access-Control-Allow-Methods` + response header are separated by a comma (","). + + A CORS-safelisted method is a method that is `GET`, `HEAD`, or `POST`. + (See https://fetch.spec.whatwg.org/#cors-safelisted-method) The + CORS-safelisted methods are always allowed, regardless of whether they + are specified in the `AllowMethods` field. + + When the `AllowMethods` field is configured with one or more methods, the + gateway must return the `Access-Control-Allow-Methods` response header + which value is present in the `AllowMethods` field. + + If the HTTP method of the `Access-Control-Request-Method` request header + is not included in the list of methods specified by the response header + `Access-Control-Allow-Methods`, it will present an error on the client + side. + + If config contains the wildcard "*" in allowMethods and the request is + not credentialed, the `Access-Control-Allow-Methods` response header + can either use the `*` wildcard or the value of + Access-Control-Request-Method from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Methods` response header. When + also the `AllowCredentials` field is true and `AllowMethods` field + specified with the `*` wildcard, the gateway must specify one HTTP method + in the value of the Access-Control-Allow-Methods response header. The + value of the header `Access-Control-Allow-Methods` is same as the + `Access-Control-Request-Method` header provided by the client. If the + header `Access-Control-Request-Method` is not included in the request, + the gateway will omit the `Access-Control-Allow-Methods` response header, + instead of specifying the `*` wildcard. + + Support: Extended + items: + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + - '*' + type: string + maxItems: 9 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowMethods cannot contain '*' alongside + other methods + rule: '!(''*'' in self && self.size() > 1)' + allowOrigins: + description: |- + AllowOrigins indicates whether the response can be shared with requested + resource from the given `Origin`. + + The `Origin` consists of a scheme and a host, with an optional port, and + takes the form `<scheme>://<host>(:<port>)`. + + Valid values for scheme are: `http` and `https`. + + Valid values for port are any integer between 1 and 65535 (the list of + available TCP/UDP ports). Note that, if not included, port `80` is + assumed for `http` scheme origins, and port `443` is assumed for `https` + origins. This may affect origin matching. + + The host part of the origin may contain the wildcard character `*`. These + wildcard characters behave as follows: + + * `*` is a greedy match to the _left_, including any number of + DNS labels to the left of its position. This also means that + `*` will include any number of period `.` characters to the + left of its position. + * A wildcard by itself matches all hosts. + + An origin value that includes _only_ the `*` character indicates requests + from all `Origin`s are allowed. + + When the `AllowOrigins` field is configured with multiple origins, it + means the server supports clients from multiple origins. If the request + `Origin` matches the configured allowed origins, the gateway must return + the given `Origin` and sets value of the header + `Access-Control-Allow-Origin` same as the `Origin` header provided by the + client. + + The status code of a successful response to a "preflight" request is + always an OK status (i.e., 204 or 200). + + If the request `Origin` does not match the configured allowed origins, + the gateway returns 204/200 response but doesn't set the relevant + cross-origin response headers. Alternatively, the gateway responds with + 403 status to the "preflight" request is denied, coupled with omitting + the CORS headers. The cross-origin request fails on the client side. + Therefore, the client doesn't attempt the actual cross-origin request. + + Conversely, if the request `Origin` matches one of the configured + allowed origins, the gateway sets the response header + `Access-Control-Allow-Origin` to the same value as the `Origin` + header provided by the client. + + When config has the wildcard ("*") in allowOrigins, and the request + is not credentialed (e.g., it is a preflight request), the + `Access-Control-Allow-Origin` response header either contains the + wildcard as well or the Origin from the request. + + When the request is credentialed, the gateway must not specify the `*` + wildcard in the `Access-Control-Allow-Origin` response header. When + also the `AllowCredentials` field is true and `AllowOrigins` field + specified with the `*` wildcard, the gateway must return a single origin + in the value of the `Access-Control-Allow-Origin` response header, + instead of specifying the `*` wildcard. The value of the header + `Access-Control-Allow-Origin` is same as the `Origin` header provided by + the client. + + Support: Extended + items: + description: |- + The CORSOrigin MUST NOT be a relative URI, and it MUST follow the URI syntax and + encoding rules specified in RFC3986. The CORSOrigin MUST include both a + scheme ("http" or "https") and a scheme-specific-part, or it should be a single '*' character. + URIs that include an authority MUST include a fully qualified domain name or + IP address as the host. + maxLength: 253 + minLength: 1 + pattern: (^\*$)|(^(http(s)?):\/\/(((\*\.)?([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9-]+|\*)(:([0-9]{1,5}))?)$) + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + x-kubernetes-validations: + - message: AllowOrigins cannot contain '*' alongside + other origins + rule: '!(''*'' in self && self.size() > 1)' + exposeHeaders: + description: |- + ExposeHeaders indicates which HTTP response headers can be exposed + to client-side scripts in response to a cross-origin request. + + A CORS-safelisted response header is an HTTP header in a CORS response + that it is considered safe to expose to the client scripts. + The CORS-safelisted response headers include the following headers: + `Cache-Control` + `Content-Language` + `Content-Length` + `Content-Type` + `Expires` + `Last-Modified` + `Pragma` + (See https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name) + The CORS-safelisted response headers are exposed to client by default. + + When an HTTP header name is specified using the `ExposeHeaders` field, + this additional header will be exposed as part of the response to the + client. + + Header names are not case-sensitive. + + Multiple header names in the value of the `Access-Control-Expose-Headers` + response header are separated by a comma (","). + + A wildcard indicates that the responses with all HTTP headers are exposed + to clients. The `Access-Control-Expose-Headers` response header can only + use `*` wildcard as value when the request is not credentialed. + + When the `exposeHeaders` config field contains the "*" wildcard and + the request is credentialed, the gateway cannot use the `*` wildcard in + the `Access-Control-Expose-Headers` response header. + + Support: Extended + items: + description: |- + HTTPHeaderName is the name of an HTTP header. + + Valid values include: + + * "Authorization" + * "Set-Cookie" + + Invalid values include: + + - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo + headers are not currently supported by this type. + - "/invalid" - "/ " is an invalid character + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + maxItems: 64 + type: array + x-kubernetes-list-type: set + maxAge: + default: 5 + description: |- + MaxAge indicates the duration (in seconds) for the client to cache the + results of a "preflight" request. + + The information provided by the `Access-Control-Allow-Methods` and + `Access-Control-Allow-Headers` response headers can be cached by the + client until the time specified by `Access-Control-Max-Age` elapses. + + The default value of `Access-Control-Max-Age` response header is 5 + (seconds). + + When the `MaxAge` field is unspecified, the gateway sets the response + header "Access-Control-Max-Age: 5" by default. + format: int32 + minimum: 1 + type: integer + type: object + extensionRef: + description: |- + ExtensionRef is an optional, implementation-specific extension to the + "filter" behavior. For example, resource "myroutefilter" in group + "networking.example.net"). ExtensionRef MUST NOT be used for core and + extended filters. + + This filter can be used multiple times within the same rule. + + Support: Implementation-specific + properties: + group: + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + - name + type: object + requestHeaderModifier: + description: |- + RequestHeaderModifier defines a schema for a filter that modifies request + headers. + + Support: Core + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + requestMirror: + description: |- + RequestMirror defines a schema for a filter that mirrors requests. + Requests are sent to the specified destination, but responses from + that destination are ignored. + + This filter can be used multiple times within the same rule. Note that + not all implementations will be able to support mirroring to multiple + backends. + + Support: Extended + properties: + backendRef: + description: |- + BackendRef references a resource where mirrored requests are sent. + + Mirrored requests must be sent only to a single destination endpoint + within this BackendRef, irrespective of how many endpoints are present + within this BackendRef. + + If the referent cannot be found, this BackendRef is invalid and must be + dropped from the Gateway. The controller must ensure the "ResolvedRefs" + condition on the Route status is set to `status: False` and not configure + this backend in the underlying implementation. + + If there is a cross-namespace reference to an *existing* object + that is not allowed by a ReferenceGrant, the controller must ensure the + "ResolvedRefs" condition on the Route is set to `status: False`, + with the "RefNotPermitted" reason and not configure this backend in the + underlying implementation. + + In either error case, the Message of the `ResolvedRefs` Condition + should be used to provide more detail about the problem. + + Support: Extended for Kubernetes Service + + Support: Implementation-specific for any other resource + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + fraction: + description: |- + Fraction represents the fraction of requests that should be + mirrored to BackendRef. + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + properties: + denominator: + default: 100 + format: int32 + minimum: 1 + type: integer + numerator: + format: int32 + minimum: 0 + type: integer + required: + - numerator + type: object + x-kubernetes-validations: + - message: numerator must be less than or equal to + denominator + rule: self.numerator <= self.denominator + percent: + description: |- + Percent represents the percentage of requests that should be + mirrored to BackendRef. Its minimum value is 0 (indicating 0% of + requests) and its maximum value is 100 (indicating 100% of requests). + + Only one of Fraction or Percent may be specified. If neither field + is specified, 100% of requests will be mirrored. + format: int32 + maximum: 100 + minimum: 0 + type: integer + required: + - backendRef + type: object + x-kubernetes-validations: + - message: Only one of percent or fraction may be specified + in HTTPRequestMirrorFilter + rule: '!(has(self.percent) && has(self.fraction))' + requestRedirect: + description: |- + RequestRedirect defines a schema for a filter that responds to the + request with an HTTP redirection. + + Support: Core + properties: + hostname: + description: |- + Hostname is the hostname to be used in the value of the `Location` + header in the response. + When empty, the hostname in the `Host` header of the request is used. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines parameters used to modify the path of the incoming request. + The modified path is then used to construct the `Location` header. When + empty, the request path is used as-is. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + port: + description: |- + Port is the port to be used in the value of the `Location` + header in the response. + + If no port is specified, the redirect port MUST be derived using the + following rules: + + * If redirect scheme is not-empty, the redirect port MUST be the well-known + port associated with the redirect scheme. Specifically "http" to port 80 + and "https" to port 443. If the redirect scheme does not have a + well-known port, the listener port of the Gateway SHOULD be used. + * If redirect scheme is empty, the redirect port MUST be the Gateway + Listener port. + + Implementations SHOULD NOT add the port number in the 'Location' + header in the following cases: + + * A Location header that will use HTTP (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 80. + * A Location header that will use HTTPS (whether that is determined via + the Listener protocol or the Scheme field) _and_ use port 443. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + scheme: + description: |- + Scheme is the scheme to be used in the value of the `Location` header in + the response. When empty, the scheme of the request is used. + + Scheme redirects can affect the port of the redirect, for more information, + refer to the documentation for the port field of this filter. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Extended + enum: + - http + - https + type: string + statusCode: + default: 302 + description: |- + StatusCode is the HTTP status code to be used in response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + + Support: Core + enum: + - 301 + - 302 + - 303 + - 307 + - 308 + type: integer + type: object + responseHeaderModifier: + description: |- + ResponseHeaderModifier defines a schema for a filter that modifies response + headers. + + Support: Extended + properties: + add: + description: |- + Add adds the given header(s) (name, value) to the request + before the action. It appends to any existing values associated + with the header name. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + add: + - name: "my-header" + value: "bar,baz" + + Output: + GET /foo HTTP/1.1 + my-header: foo,bar,baz + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + remove: + description: |- + Remove the given header(s) from the HTTP request before the action. The + value of Remove is a list of HTTP header names. Note that the header + names are case-insensitive (see + https://datatracker.ietf.org/doc/html/rfc2616#section-4.2). + + Input: + GET /foo HTTP/1.1 + my-header1: foo + my-header2: bar + my-header3: baz + + Config: + remove: ["my-header1", "my-header3"] + + Output: + GET /foo HTTP/1.1 + my-header2: bar + items: + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: set + set: + description: |- + Set overwrites the request with the given header (name, value) + before the action. + + Input: + GET /foo HTTP/1.1 + my-header: foo + + Config: + set: + - name: "my-header" + value: "bar" + + Output: + GET /foo HTTP/1.1 + my-header: bar + items: + description: HTTPHeader represents an HTTP Header + name and value as defined by RFC 7230. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, the first entry with + an equivalent name MUST be considered for a match. Subsequent entries + with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + value: + description: Value is the value of HTTP Header + to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: + description: |- + Type identifies the type of filter to apply. As with other API fields, + types are classified into three conformance levels: + + - Core: Filter types and their corresponding configuration defined by + "Support: Core" in this package, e.g. "RequestHeaderModifier". All + implementations must support core filters. + + - Extended: Filter types and their corresponding configuration defined by + "Support: Extended" in this package, e.g. "RequestMirror". Implementers + are encouraged to support extended filters. + + - Implementation-specific: Filters that are defined and supported by + specific vendors. + In the future, filters showing convergence in behavior across multiple + implementations will be considered for inclusion in extended or core + conformance levels. Filter-specific configuration for such filters + is specified using the ExtensionRef field. `Type` should be set to + "ExtensionRef" for custom filters. + + Implementers are encouraged to define custom implementation types to + extend the core API with implementation-specific behavior. + + If a reference to a custom filter type cannot be resolved, the filter + MUST NOT be skipped. Instead, requests that would have been processed by + that filter MUST receive a HTTP error response. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - RequestHeaderModifier + - ResponseHeaderModifier + - RequestMirror + - RequestRedirect + - URLRewrite + - ExtensionRef + - CORS + type: string + urlRewrite: + description: |- + URLRewrite defines a schema for a filter that modifies a request during forwarding. + + Support: Extended + properties: + hostname: + description: |- + Hostname is the value to be used to replace the Host header value during + forwarding. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + path: + description: |- + Path defines a path rewrite. + + Support: Extended + properties: + replaceFullPath: + description: |- + ReplaceFullPath specifies the value with which to replace the full path + of a request during a rewrite or redirect. + maxLength: 1024 + type: string + replacePrefixMatch: + description: |- + ReplacePrefixMatch specifies the value with which to replace the prefix + match of a request during a rewrite or redirect. For example, a request + to "/foo/bar" with a prefix match of "/foo" and a ReplacePrefixMatch + of "/xyz" would be modified to "/xyz/bar". + + Note that this matches the behavior of the PathPrefix match type. This + matches full path elements. A path element refers to the list of labels + in the path split by the `/` separator. When specified, a trailing `/` is + ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all + match the prefix `/abc`, but the path `/abcd` would not. + + ReplacePrefixMatch is only compatible with a `PathPrefix` HTTPRouteMatch. + Using any other HTTPRouteMatch type on the same HTTPRouteRule will result in + the implementation setting the Accepted Condition for the Route to `status: False`. + + Request Path | Prefix Match | Replace Prefix | Modified Path + maxLength: 1024 + type: string + type: + description: |- + Type defines the type of path modifier. Additional types may be + added in a future release of the API. + + Note that values may be added to this enum, implementations + must ensure that unknown values will not cause a crash. + + Unknown values here must result in the implementation setting the + Accepted Condition for the Route to `status: False`, with a + Reason of `UnsupportedValue`. + enum: + - ReplaceFullPath + - ReplacePrefixMatch + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: replaceFullPath must be specified when + type is set to 'ReplaceFullPath' + rule: 'self.type == ''ReplaceFullPath'' ? has(self.replaceFullPath) + : true' + - message: type must be 'ReplaceFullPath' when replaceFullPath + is set + rule: 'has(self.replaceFullPath) ? self.type == + ''ReplaceFullPath'' : true' + - message: replacePrefixMatch must be specified when + type is set to 'ReplacePrefixMatch' + rule: 'self.type == ''ReplacePrefixMatch'' ? has(self.replacePrefixMatch) + : true' + - message: type must be 'ReplacePrefixMatch' when + replacePrefixMatch is set + rule: 'has(self.replacePrefixMatch) ? self.type + == ''ReplacePrefixMatch'' : true' + type: object + required: + - type + type: object + x-kubernetes-validations: + - message: filter.cors must be nil if the filter.type is not + CORS + rule: '!(has(self.cors) && self.type != ''CORS'')' + - message: filter.cors must be specified for CORS filter.type + rule: '!(!has(self.cors) && self.type == ''CORS'')' + - message: filter.requestHeaderModifier must be nil if the + filter.type is not RequestHeaderModifier + rule: '!(has(self.requestHeaderModifier) && self.type != + ''RequestHeaderModifier'')' + - message: filter.requestHeaderModifier must be specified + for RequestHeaderModifier filter.type + rule: '!(!has(self.requestHeaderModifier) && self.type == + ''RequestHeaderModifier'')' + - message: filter.responseHeaderModifier must be nil if the + filter.type is not ResponseHeaderModifier + rule: '!(has(self.responseHeaderModifier) && self.type != + ''ResponseHeaderModifier'')' + - message: filter.responseHeaderModifier must be specified + for ResponseHeaderModifier filter.type + rule: '!(!has(self.responseHeaderModifier) && self.type + == ''ResponseHeaderModifier'')' + - message: filter.requestMirror must be nil if the filter.type + is not RequestMirror + rule: '!(has(self.requestMirror) && self.type != ''RequestMirror'')' + - message: filter.requestMirror must be specified for RequestMirror + filter.type + rule: '!(!has(self.requestMirror) && self.type == ''RequestMirror'')' + - message: filter.requestRedirect must be nil if the filter.type + is not RequestRedirect + rule: '!(has(self.requestRedirect) && self.type != ''RequestRedirect'')' + - message: filter.requestRedirect must be specified for RequestRedirect + filter.type + rule: '!(!has(self.requestRedirect) && self.type == ''RequestRedirect'')' + - message: filter.urlRewrite must be nil if the filter.type + is not URLRewrite + rule: '!(has(self.urlRewrite) && self.type != ''URLRewrite'')' + - message: filter.urlRewrite must be specified for URLRewrite + filter.type + rule: '!(!has(self.urlRewrite) && self.type == ''URLRewrite'')' + - message: filter.extensionRef must be nil if the filter.type + is not ExtensionRef + rule: '!(has(self.extensionRef) && self.type != ''ExtensionRef'')' + - message: filter.extensionRef must be specified for ExtensionRef + filter.type + rule: '!(!has(self.extensionRef) && self.type == ''ExtensionRef'')' + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: May specify either httpRouteFilterRequestRedirect + or httpRouteFilterRequestRewrite, but not both + rule: '!(self.exists(f, f.type == ''RequestRedirect'') && + self.exists(f, f.type == ''URLRewrite''))' + - message: RequestHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'RequestHeaderModifier').size() + <= 1 + - message: ResponseHeaderModifier filter cannot be repeated + rule: self.filter(f, f.type == 'ResponseHeaderModifier').size() + <= 1 + - message: RequestRedirect filter cannot be repeated + rule: self.filter(f, f.type == 'RequestRedirect').size() <= + 1 + - message: URLRewrite filter cannot be repeated + rule: self.filter(f, f.type == 'URLRewrite').size() <= 1 + matches: + default: + - path: + type: PathPrefix + value: / + description: |- + Matches define conditions used for matching the rule against incoming + HTTP requests. Each match is independent, i.e. this rule will be matched + if **any** one of the matches is satisfied. + + For example, take the following matches configuration: + + ``` + matches: + - path: + value: "/foo" + headers: + - name: "version" + value: "v2" + - path: + value: "/v2/foo" + ``` + + For a request to match against this rule, a request must satisfy + EITHER of the two conditions: + + - path prefixed with `/foo` AND contains the header `version: v2` + - path prefix of `/v2/foo` + + See the documentation for HTTPRouteMatch on how to specify multiple + match conditions that should be ANDed together. + + If no matches are specified, the default is a prefix + path match on "/", which has the effect of matching every + HTTP request. + + Proxy or Load Balancer routing configuration generated from HTTPRoutes + MUST prioritize matches based on the following criteria, continuing on + ties. Across all rules specified on applicable Routes, precedence must be + given to the match having: + + * "Exact" path match. + * "Prefix" path match with largest number of characters. + * Method match. + * Largest number of header matches. + * Largest number of query param matches. + + Note: The precedence of RegularExpression path matches are implementation-specific. + + If ties still exist across multiple Routes, matching precedence MUST be + determined in order of the following criteria, continuing on ties: + + * The oldest Route based on creation timestamp. + * The Route appearing first in alphabetical order by + "{namespace}/{name}". + + If ties still exist within an HTTPRoute, matching precedence MUST be granted + to the FIRST matching rule (in list order) with a match meeting the above + criteria. + + When no rules matching a request have been successfully attached to the + parent a request is coming from, a HTTP 404 status code MUST be returned. + items: + description: "HTTPRouteMatch defines the predicate used to + match requests to a given\naction. Multiple match types + are ANDed together, i.e. the match will\nevaluate to true + only if all conditions are satisfied.\n\nFor example, the + match below will match a HTTP request only if its path\nstarts + with `/foo` AND it contains the `version: v1` header:\n\n```\nmatch:\n\n\tpath:\n\t + \ value: \"/foo\"\n\theaders:\n\t- name: \"version\"\n\t + \ value \"v1\"\n\n```" + properties: + headers: + description: |- + Headers specifies HTTP request header matchers. Multiple match values are + ANDed together, meaning, a request must match all the specified headers + to select the route. + items: + description: |- + HTTPHeaderMatch describes how to select a HTTP route by matching HTTP request + headers. + properties: + name: + description: |- + Name is the name of the HTTP Header to be matched. Name matching MUST be + case-insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + + If multiple entries specify equivalent header names, only the first + entry with an equivalent name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be ignored. Due to the + case-insensitivity of header names, "foo" and "Foo" are considered + equivalent. + + When a header is repeated in an HTTP request, it is + implementation-specific behavior as to how this is represented. + Generally, proxies should follow the guidance from the RFC: + https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 regarding + processing a repeated header, with special handling for "Set-Cookie". + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: |- + Type specifies how to match against the value of the header. + + Support: Core (Exact) + + Support: Implementation-specific (RegularExpression) + + Since RegularExpression HeaderMatchType has implementation-specific + conformance, implementations can support POSIX, PCRE or any other dialects + of regular expressions. Please read the implementation's documentation to + determine the supported dialect. + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: |- + Method specifies HTTP method matcher. + When specified, this route will be matched only if the request has the + specified method. + + Support: Extended + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: |- + Path specifies a HTTP request path matcher. If this field is not + specified, a default prefix match on the "/" path is provided. + properties: + type: + default: PathPrefix + description: |- + Type specifies how to match against the path Value. + + Support: Core (Exact, PathPrefix) + + Support: Implementation-specific (RegularExpression) + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + x-kubernetes-validations: + - message: value must be an absolute path and start with + '/' when type one of ['Exact', 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? self.value.startsWith(''/'') + : true' + - message: must not contain '//' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''//'') + : true' + - message: must not contain '/./' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''/./'') + : true' + - message: must not contain '/../' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''/../'') + : true' + - message: must not contain '%2f' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''%2f'') + : true' + - message: must not contain '%2F' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''%2F'') + : true' + - message: must not contain '#' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.contains(''#'') + : true' + - message: must not end with '/..' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.endsWith(''/..'') + : true' + - message: must not end with '/.' when type one of ['Exact', + 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? !self.value.endsWith(''/.'') + : true' + - message: type must be one of ['Exact', 'PathPrefix', + 'RegularExpression'] + rule: self.type in ['Exact','PathPrefix'] || self.type + == 'RegularExpression' + - message: must only contain valid characters (matching + ^(?:[-A-Za-z0-9/._~!$&'()*+,;=:@]|[%][0-9a-fA-F]{2})+$) + for types ['Exact', 'PathPrefix'] + rule: '(self.type in [''Exact'',''PathPrefix'']) ? self.value.matches(r"""^(?:[-A-Za-z0-9/._~!$&''()*+,;=:@]|[%][0-9a-fA-F]{2})+$""") + : true' + queryParams: + description: |- + QueryParams specifies HTTP query parameter matchers. Multiple match + values are ANDed together, meaning, a request must match all the + specified query parameters to select the route. + + Support: Extended + items: + description: |- + HTTPQueryParamMatch describes how to select a HTTP route by matching HTTP + query parameters. + properties: + name: + description: |- + Name is the name of the HTTP query param to be matched. This must be an + exact string match. (See + https://tools.ietf.org/html/rfc7230#section-2.7.3). + + If multiple entries specify equivalent query param names, only the first + entry with an equivalent name MUST be considered for a match. Subsequent + entries with an equivalent query param name MUST be ignored. + + If a query param is repeated in an HTTP request, the behavior is + purposely left undefined, since different data planes have different + capabilities. However, it is *recommended* that implementations should + match against the first value of the param if the data plane supports it, + as this behavior is expected in other load balancing contexts outside of + the Gateway API. + + Users SHOULD NOT route traffic based on repeated query params to guard + themselves against potential differences in the implementations. + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: |- + Type specifies how to match against the value of the query parameter. + + Support: Extended (Exact) + + Support: Implementation-specific (RegularExpression) + + Since RegularExpression QueryParamMatchType has Implementation-specific + conformance, implementations can support POSIX, PCRE or any other + dialects of regular expressions. Please read the implementation's + documentation to determine the supported dialect. + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + name: + description: |- + Name is the name of the route rule. This name MUST be unique within a Route if it is set. + + Support: Extended + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + timeouts: + description: |- + Timeouts defines the timeouts that can be configured for an HTTP request. + + Support: Extended + properties: + backendRequest: + description: |- + BackendRequest specifies a timeout for an individual request from the gateway + to a backend. This covers the time from when the request first starts being + sent from the gateway to when the full response has been received from the backend. + + Setting a timeout to the zero duration (e.g. "0s") SHOULD disable the timeout + completely. Implementations that cannot completely disable the timeout MUST + instead interpret the zero duration as the longest possible value to which + the timeout can be set. + + An entire client HTTP transaction with a gateway, covered by the Request timeout, + may result in more than one call from the gateway to the destination backend, + for example, if automatic retries are supported. + + The value of BackendRequest must be a Gateway API Duration string as defined by + GEP-2257. When this field is unspecified, its behavior is implementation-specific; + when specified, the value of BackendRequest must be no more than the value of the + Request timeout (since the Request timeout encompasses the BackendRequest timeout). + + Support: Extended + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + request: + description: |- + Request specifies the maximum duration for a gateway to respond to an HTTP request. + If the gateway has not been able to respond before this deadline is met, the gateway + MUST return a timeout error. + + For example, setting the `rules.timeouts.request` field to the value `10s` in an + `HTTPRoute` will cause a timeout if a client request is taking longer than 10 seconds + to complete. + + Setting a timeout to the zero duration (e.g. "0s") SHOULD disable the timeout + completely. Implementations that cannot completely disable the timeout MUST + instead interpret the zero duration as the longest possible value to which + the timeout can be set. + + This timeout is intended to cover as close to the whole request-response transaction + as possible although an implementation MAY choose to start the timeout after the entire + request stream has been received instead of immediately after the transaction is + initiated by the client. + + The value of Request is a Gateway API Duration string as defined by GEP-2257. When this + field is unspecified, request timeout behavior is implementation-specific. + + Support: Extended + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + x-kubernetes-validations: + - message: backendRequest timeout cannot be longer than request + timeout + rule: '!(has(self.request) && has(self.backendRequest) && + duration(self.request) != duration(''0s'') && duration(self.backendRequest) + > duration(self.request))' + type: object + x-kubernetes-validations: + - message: RequestRedirect filter must not be used together with + backendRefs + rule: '(has(self.backendRefs) && size(self.backendRefs) > 0) ? + (!has(self.filters) || self.filters.all(f, !has(f.requestRedirect))): + true' + - message: When using RequestRedirect filter with path.replacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + ? ((size(self.matches) != 1 || !has(self.matches[0].path) || + self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: When using URLRewrite filter with path.replacePrefixMatch, + exactly one PathPrefix match must be specified + rule: '(has(self.filters) && self.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' + - message: Within backendRefs, when using RequestRedirect filter + with path.replacePrefixMatch, exactly one PathPrefix match must + be specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.requestRedirect) + && has(f.requestRedirect.path) && f.requestRedirect.path.type + == ''ReplacePrefixMatch'' && has(f.requestRedirect.path.replacePrefixMatch))) + )) ? ((size(self.matches) != 1 || !has(self.matches[0].path) + || self.matches[0].path.type != ''PathPrefix'') ? false : true) + : true' + - message: Within backendRefs, When using URLRewrite filter with + path.replacePrefixMatch, exactly one PathPrefix match must be + specified + rule: '(has(self.backendRefs) && self.backendRefs.exists_one(b, + (has(b.filters) && b.filters.exists_one(f, has(f.urlRewrite) + && has(f.urlRewrite.path) && f.urlRewrite.path.type == ''ReplacePrefixMatch'' + && has(f.urlRewrite.path.replacePrefixMatch))) )) ? ((size(self.matches) + != 1 || !has(self.matches[0].path) || self.matches[0].path.type + != ''PathPrefix'') ? false : true) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: While 16 rules and 64 matches per rule are allowed, the + total number of matches across all rules in a route must be less + than 128 + rule: '(self.size() > 0 ? self[0].matches.size() : 0) + (self.size() + > 1 ? self[1].matches.size() : 0) + (self.size() > 2 ? self[2].matches.size() + : 0) + (self.size() > 3 ? self[3].matches.size() : 0) + (self.size() + > 4 ? self[4].matches.size() : 0) + (self.size() > 5 ? self[5].matches.size() + : 0) + (self.size() > 6 ? self[6].matches.size() : 0) + (self.size() + > 7 ? self[7].matches.size() : 0) + (self.size() > 8 ? self[8].matches.size() + : 0) + (self.size() > 9 ? self[9].matches.size() : 0) + (self.size() + > 10 ? self[10].matches.size() : 0) + (self.size() > 11 ? self[11].matches.size() + : 0) + (self.size() > 12 ? self[12].matches.size() : 0) + (self.size() + > 13 ? self[13].matches.size() : 0) + (self.size() > 14 ? self[14].matches.size() + : 0) + (self.size() > 15 ? self[15].matches.size() : 0) <= 128' + type: object + status: + description: Status defines the current state of HTTPRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: false + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_listenersets.yaml b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_listenersets.yaml new file mode 100644 index 00000000..de494f51 --- /dev/null +++ b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_listenersets.yaml @@ -0,0 +1,780 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.0 + gateway.networking.k8s.io/channel: standard + name: listenersets.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: ListenerSet + listKind: ListenerSetList + plural: listenersets + shortNames: + - lset + singular: listenerset + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="Accepted")].status + name: Accepted + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + ListenerSet defines a set of additional listeners to attach to an existing Gateway. + This resource provides a mechanism to merge multiple listeners into a single Gateway. + + The parent Gateway must explicitly allow ListenerSet attachment through its + AllowedListeners configuration. By default, Gateways do not allow ListenerSet + attachment. + + Routes can attach to a ListenerSet by specifying it as a parentRef, and can + optionally target specific listeners using the sectionName field. + + Policy Attachment: + - Policies that attach to a ListenerSet apply to all listeners defined in that resource + - Policies do not impact listeners in the parent Gateway + - Different ListenerSets attached to the same Gateway can have different policies + - If an implementation cannot apply a policy to specific listeners, it should reject the policy + + ReferenceGrant Semantics: + - ReferenceGrants applied to a Gateway are not inherited by child ListenerSets + - ReferenceGrants applied to a ListenerSet do not grant permission to the parent Gateway's listeners + - A ListenerSet can reference secrets/backends in its own namespace without a ReferenceGrant + + Gateway Integration: + - The parent Gateway's status will include "AttachedListenerSets" + which is the count of ListenerSets that have successfully attached to a Gateway + A ListenerSet is successfully attached to a Gateway when all the following conditions are met: + - The ListenerSet is selected by the Gateway's AllowedListeners field + - The ListenerSet has a valid ParentRef selecting the Gateway + - The ListenerSet's status has the condition "Accepted: true" + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of ListenerSet. + properties: + listeners: + description: |- + Listeners associated with this ListenerSet. Listeners define + logical endpoints that are bound on this referenced parent Gateway's addresses. + + Listeners in a `Gateway` and their attached `ListenerSets` are concatenated + as a list when programming the underlying infrastructure. Each listener + name does not need to be unique across the Gateway and ListenerSets. + See ListenerEntry.Name for more details. + + Implementations MUST treat the parent Gateway as having the merged + list of all listeners from itself and attached ListenerSets using + the following precedence: + + 1. "parent" Gateway + 2. ListenerSet ordered by creation time (oldest first) + 3. ListenerSet ordered alphabetically by "{namespace}/{name}". + + An implementation MAY reject listeners by setting the ListenerEntryStatus + `Accepted` condition to False with the Reason `TooManyListeners` + + If a listener has a conflict, this will be reported in the + Status.ListenerEntryStatus setting the `Conflicted` condition to True. + + Implementations SHOULD be cautious about what information from the + parent or siblings are reported to avoid accidentally leaking + sensitive information that the child would not otherwise have access + to. This can include contents of secrets etc. + items: + properties: + allowedRoutes: + default: + namespaces: + from: Same + description: |- + AllowedRoutes defines the types of routes that MAY be attached to a + Listener and the trusted namespaces where those Route resources MAY be + present. + + Although a client request may match multiple route rules, only one rule + may ultimately receive the request. Matching precedence MUST be + determined in order of the following criteria: + + * The most specific match as defined by the Route type. + * The oldest Route based on creation timestamp. For example, a Route with + a creation timestamp of "2020-09-08 01:02:03" is given precedence over + a Route with a creation timestamp of "2020-09-08 01:02:04". + * If everything else is equivalent, the Route appearing first in + alphabetical order (namespace/name) should be given precedence. For + example, foo/bar is given precedence over foo/baz. + + All valid rules within a Route attached to this Listener should be + implemented. Invalid Route rules can be ignored (sometimes that will mean + the full Route). If a Route rule transitions from valid to invalid, + support for that Route rule should be dropped to ensure consistency. For + example, even if a filter specified by a Route rule is invalid, the rest + of the rules within that Route should still be supported. + properties: + kinds: + description: |- + Kinds specifies the groups and kinds of Routes that are allowed to bind + to this Gateway Listener. When unspecified or empty, the kinds of Routes + selected are determined using the Listener protocol. + + A RouteGroupKind MUST correspond to kinds of Routes that are compatible + with the application protocol specified in the Listener's Protocol field. + If an implementation does not support or recognize this resource type, it + MUST set the "ResolvedRefs" condition to False for this Listener with the + "InvalidRouteKinds" reason. + + Support: Core + items: + description: RouteGroupKind indicates the group and kind + of a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + namespaces: + default: + from: Same + description: |- + Namespaces indicates namespaces from which Routes may be attached to this + Listener. This is restricted to the namespace of this Gateway by default. + + Support: Core + properties: + from: + default: Same + description: |- + From indicates where Routes will be selected for this Gateway. Possible + values are: + + * All: Routes in all namespaces may be used by this Gateway. + * Selector: Routes in namespaces selected by the selector may be used by + this Gateway. + * Same: Only Routes in the same namespace may be used by this Gateway. + + Support: Core + enum: + - All + - Selector + - Same + type: string + selector: + description: |- + Selector must be specified when From is set to "Selector". In that case, + only Routes in Namespaces matching this Selector will be selected by this + Gateway. This field is ignored for other values of "From". + + Support: Core + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: object + hostname: + description: |- + Hostname specifies the virtual hostname to match for protocol types that + define this concept. When unspecified, all hostnames are matched. This + field is ignored for protocols that don't require hostname based + matching. + + Implementations MUST apply Hostname matching appropriately for each of + the following protocols: + + * TLS: The Listener Hostname MUST match the SNI. + * HTTP: The Listener Hostname MUST match the Host header of the request. + * HTTPS: The Listener Hostname SHOULD match at both the TLS and HTTP + protocol layers as described above. If an implementation does not + ensure that both the SNI and Host header match the Listener hostname, + it MUST clearly document that. + + For HTTPRoute and TLSRoute resources, there is an interaction with the + `spec.hostnames` array. When both listener and route specify hostnames, + there MUST be an intersection between the values for a Route to be + accepted. For more information, refer to the Route specific Hostnames + documentation. + + Hostnames that are prefixed with a wildcard label (`*.`) are interpreted + as a suffix match. That means that a match for `*.example.com` would match + both `test.example.com`, and `foo.test.example.com`, but not `example.com`. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + name: + description: |- + Name is the name of the Listener. This name MUST be unique within a + ListenerSet. + + Name is not required to be unique across a Gateway and ListenerSets. + Routes can attach to a Listener by having a ListenerSet as a parentRef + and setting the SectionName + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + port: + description: |- + Port is the network port. Multiple listeners may use the + same port, subject to the Listener compatibility rules. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + protocol: + description: Protocol specifies the network protocol this listener + expects to receive. + maxLength: 255 + minLength: 1 + pattern: ^[a-zA-Z0-9]([-a-zA-Z0-9]*[a-zA-Z0-9])?$|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9]+$ + type: string + tls: + description: |- + TLS is the TLS configuration for the Listener. This field is required if + the Protocol field is "HTTPS" or "TLS". It is invalid to set this field + if the Protocol field is "HTTP", "TCP", or "UDP". + + The association of SNIs to Certificate defined in ListenerTLSConfig is + defined based on the Hostname field for this listener. + + The GatewayClass MUST use the longest matching SNI out of all + available certificates for any TLS handshake. + properties: + certificateRefs: + description: |- + CertificateRefs contains a series of references to Kubernetes objects that + contains TLS certificates and private keys. These certificates are used to + establish a TLS handshake for requests that match the hostname of the + associated listener. + + A single CertificateRef to a Kubernetes Secret has "Core" support. + Implementations MAY choose to support attaching multiple certificates to + a Listener, but this behavior is implementation-specific. + + References to a resource in different namespace are invalid UNLESS there + is a ReferenceGrant in the target namespace that allows the certificate + to be attached. If a ReferenceGrant does not allow this reference, the + "ResolvedRefs" condition MUST be set to False for this listener with the + "RefNotPermitted" reason. + + This field is required to have at least one element when the mode is set + to "Terminate" (default) and is optional otherwise. + + CertificateRefs can reference to standard Kubernetes resources, i.e. + Secret, or implementation-specific custom resources. + + Support: Core - A single reference to a Kubernetes Secret of type kubernetes.io/tls + + Support: Implementation-specific (More than one reference or other resource types) + items: + description: |- + SecretObjectReference identifies an API object including its namespace, + defaulting to Secret. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + + References to objects with invalid Group and Kind are not valid, and must + be rejected by the implementation, with appropriate Conditions set + on the containing object. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Secret + description: Kind is kind of the referent. For example + "Secret". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referenced object. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + mode: + default: Terminate + description: |- + Mode defines the TLS behavior for the TLS session initiated by the client. + There are two possible modes: + + - Terminate: The TLS session between the downstream client and the + Gateway is terminated at the Gateway. This mode requires certificates + to be specified in some way, such as populating the certificateRefs + field. + - Passthrough: The TLS session is NOT terminated by the Gateway. This + implies that the Gateway can't decipher the TLS stream except for + the ClientHello message of the TLS protocol. The certificateRefs field + is ignored in this mode. + + Support: Core + enum: + - Terminate + - Passthrough + type: string + options: + additionalProperties: + description: |- + AnnotationValue is the value of an annotation in Gateway API. This is used + for validation of maps such as TLS options. This roughly matches Kubernetes + annotation validation, although the length validation in that case is based + on the entire size of the annotations struct. + maxLength: 4096 + minLength: 0 + type: string + description: |- + Options are a list of key/value pairs to enable extended TLS + configuration for each implementation. For example, configuring the + minimum TLS version or supported cipher suites. + + A set of common keys MAY be defined by the API in the future. To avoid + any ambiguity, implementation-specific definitions MUST use + domain-prefixed names, such as `example.com/my-custom-option`. + Un-prefixed names are reserved for key names defined by Gateway API. + + Support: Implementation-specific + maxProperties: 16 + type: object + type: object + x-kubernetes-validations: + - message: certificateRefs or options must be specified when + mode is Terminate + rule: 'self.mode == ''Terminate'' ? size(self.certificateRefs) + > 0 || size(self.options) > 0 : true' + required: + - name + - port + - protocol + type: object + maxItems: 64 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: tls must not be specified for protocols ['HTTP', 'TCP', + 'UDP'] + rule: 'self.all(l, l.protocol in [''HTTP'', ''TCP'', ''UDP''] ? + !has(l.tls) : true)' + - message: tls mode must be Terminate for protocol HTTPS + rule: 'self.all(l, (l.protocol == ''HTTPS'' && has(l.tls)) ? (l.tls.mode + == '''' || l.tls.mode == ''Terminate'') : true)' + - message: tls mode must be set for protocol TLS + rule: 'self.all(l, (l.protocol == ''TLS'' ? has(l.tls) && has(l.tls.mode) + && l.tls.mode != '''' : true))' + - message: hostname must not be specified for protocols ['TCP', 'UDP'] + rule: 'self.all(l, l.protocol in [''TCP'', ''UDP''] ? (!has(l.hostname) + || l.hostname == '''') : true)' + - message: Listener name must be unique within the Gateway + rule: self.all(l1, self.exists_one(l2, l1.name == l2.name)) + - message: Combination of port, protocol and hostname must be unique + for each listener + rule: 'self.all(l1, !has(l1.port) || self.exists_one(l2, has(l2.port) + && l1.port == l2.port && l1.protocol == l2.protocol && (has(l1.hostname) + && has(l2.hostname) ? l1.hostname == l2.hostname : !has(l1.hostname) + && !has(l2.hostname))))' + parentRef: + description: ParentRef references the Gateway that the listeners are + attached to. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the referent. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: Kind is kind of the referent. For example "Gateway". + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. If not present, + the namespace of the referent is assumed to be the same as + the namespace of the referring object. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object + required: + - listeners + - parentRef + type: object + status: + default: + conditions: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: Status defines the current state of ListenerSet. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Accepted + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: |- + Conditions describe the current conditions of the ListenerSet. + + Implementations MUST express ListenerSet conditions using the + `ListenerSetConditionType` and `ListenerSetConditionReason` + constants so that operators and tools can converge on a common + vocabulary to describe ListenerSet state. + + Known condition types are: + + * "Accepted" + * "Programmed" + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + listeners: + description: Listeners provide status for each unique listener port + defined in the Spec. + items: + description: ListenerStatus is the status associated with a Listener. + properties: + attachedRoutes: + description: |- + AttachedRoutes represents the total number of Routes that have been + successfully attached to this Listener. + + Successful attachment of a Route to a Listener is based solely on the + combination of the AllowedRoutes field on the corresponding Listener + and the Route's ParentRefs field. A Route is successfully attached to + a Listener when it is selected by the Listener's AllowedRoutes field + AND the Route has a valid ParentRef selecting the whole Gateway + resource or a specific Listener as a parent resource (more detail on + attachment semantics can be found in the documentation on the various + Route kinds ParentRefs fields). Listener status does not impact + successful attachment, i.e. the AttachedRoutes field count MUST be set + for Listeners, even if the Accepted condition of an individual Listener is set + to "False". The AttachedRoutes number represents the number of Routes with + the Accepted condition set to "True" that have been attached to this Listener. + Routes with any other value for the Accepted condition MUST NOT be included + in this count. + + Uses for this field include troubleshooting Route attachment and + measuring blast radius/impact of changes to a Listener. + format: int32 + type: integer + conditions: + description: Conditions describe the current condition of this + listener. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + name: + description: Name is the name of the Listener that this status + corresponds to. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + supportedKinds: + description: |- + SupportedKinds is the list indicating the Kinds supported by this + listener. This MUST represent the kinds supported by an implementation for + that Listener configuration. + + If kinds are specified in Spec that are not supported, they MUST NOT + appear in this list and an implementation MUST set the "ResolvedRefs" + condition to "False" with the "InvalidRouteKinds" reason. If both valid + and invalid Route kinds are specified, the implementation MUST + reference the valid Route kinds that have been specified. + items: + description: RouteGroupKind indicates the group and kind of + a Route resource. + properties: + group: + default: gateway.networking.k8s.io + description: Group is the group of the Route. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is the kind of the Route. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + required: + - kind + type: object + maxItems: 8 + type: array + x-kubernetes-list-type: atomic + required: + - attachedRoutes + - conditions + - name + type: object + maxItems: 64 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_referencegrants.yaml b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_referencegrants.yaml new file mode 100644 index 00000000..56ff28f5 --- /dev/null +++ b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_referencegrants.yaml @@ -0,0 +1,353 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.0 + gateway.networking.k8s.io/channel: standard + name: referencegrants.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: ReferenceGrant + listKind: ReferenceGrantList + plural: referencegrants + shortNames: + - refgrant + singular: referencegrant + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + ReferenceGrant identifies kinds of resources in other namespaces that are + trusted to reference the specified kinds of resources in the same namespace + as the policy. + + Each ReferenceGrant can be used to represent a unique trust relationship. + Additional Reference Grants can be used to add to the set of trusted + sources of inbound references for the namespace they are defined within. + + All cross-namespace references in Gateway API (with the exception of cross-namespace + Gateway-route attachment) require a ReferenceGrant. + + ReferenceGrant is a form of runtime verification allowing users to assert + which cross-namespace object references are permitted. Implementations that + support ReferenceGrant MUST NOT permit cross-namespace references which have + no grant, and MUST respond to the removal of a grant by revoking the access + that the grant allowed. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of ReferenceGrant. + properties: + from: + description: |- + From describes the trusted namespaces and kinds that can reference the + resources described in "To". Each entry in this list MUST be considered + to be an additional place that references can be valid from, or to put + this another way, entries MUST be combined using OR. + + Support: Core + items: + description: ReferenceGrantFrom describes trusted namespaces and + kinds. + properties: + group: + description: |- + Group is the group of the referent. + When empty, the Kubernetes core API group is inferred. + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: |- + Kind is the kind of the referent. Although implementations may support + additional resources, the following types are part of the "Core" + support level for this field. + + When used to permit a SecretObjectReference: + + * Gateway + + When used to permit a BackendObjectReference: + + * GRPCRoute + * HTTPRoute + * TCPRoute + * TLSRoute + * UDPRoute + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + namespace: + description: |- + Namespace is the namespace of the referent. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - namespace + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + to: + description: |- + To describes the resources that may be referenced by the resources + described in "From". Each entry in this list MUST be considered to be an + additional place that references can be valid to, or to put this another + way, entries MUST be combined using OR. + + Support: Core + items: + description: |- + ReferenceGrantTo describes what Kinds are allowed as targets of the + references. + properties: + group: + description: |- + Group is the group of the referent. + When empty, the Kubernetes core API group is inferred. + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: |- + Kind is the kind of the referent. Although implementations may support + additional resources, the following types are part of the "Core" + support level for this field: + + * Secret when used to permit a SecretObjectReference + * Service when used to permit a BackendObjectReference + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. When unspecified, this policy + refers to all resources of the specified Group and Kind in the local + namespace. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - from + - to + type: object + type: object + served: true + storage: false + subresources: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + ReferenceGrant identifies kinds of resources in other namespaces that are + trusted to reference the specified kinds of resources in the same namespace + as the policy. + + Each ReferenceGrant can be used to represent a unique trust relationship. + Additional Reference Grants can be used to add to the set of trusted + sources of inbound references for the namespace they are defined within. + + All cross-namespace references in Gateway API (with the exception of cross-namespace + Gateway-route attachment) require a ReferenceGrant. + + ReferenceGrant is a form of runtime verification allowing users to assert + which cross-namespace object references are permitted. Implementations that + support ReferenceGrant MUST NOT permit cross-namespace references which have + no grant, and MUST respond to the removal of a grant by revoking the access + that the grant allowed. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of ReferenceGrant. + properties: + from: + description: |- + From describes the trusted namespaces and kinds that can reference the + resources described in "To". Each entry in this list MUST be considered + to be an additional place that references can be valid from, or to put + this another way, entries MUST be combined using OR. + + Support: Core + items: + description: ReferenceGrantFrom describes trusted namespaces and + kinds. + properties: + group: + description: |- + Group is the group of the referent. + When empty, the Kubernetes core API group is inferred. + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: |- + Kind is the kind of the referent. Although implementations may support + additional resources, the following types are part of the "Core" + support level for this field. + + When used to permit a SecretObjectReference: + + * Gateway + + When used to permit a BackendObjectReference: + + * GRPCRoute + * HTTPRoute + * TCPRoute + * TLSRoute + * UDPRoute + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + namespace: + description: |- + Namespace is the namespace of the referent. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - group + - kind + - namespace + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + to: + description: |- + To describes the resources that may be referenced by the resources + described in "From". Each entry in this list MUST be considered to be an + additional place that references can be valid to, or to put this another + way, entries MUST be combined using OR. + + Support: Core + items: + description: |- + ReferenceGrantTo describes what Kinds are allowed as targets of the + references. + properties: + group: + description: |- + Group is the group of the referent. + When empty, the Kubernetes core API group is inferred. + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: |- + Kind is the kind of the referent. Although implementations may support + additional resources, the following types are part of the "Core" + support level for this field: + + * Secret when used to permit a SecretObjectReference + * Service when used to permit a BackendObjectReference + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. When unspecified, this policy + refers to all resources of the specified Group and Kind in the local + namespace. + maxLength: 253 + minLength: 1 + type: string + required: + - group + - kind + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - from + - to + type: object + type: object + served: true + storage: true + subresources: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_tlsroutes.yaml b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_tlsroutes.yaml new file mode 100644 index 00000000..3b9e163d --- /dev/null +++ b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_tlsroutes.yaml @@ -0,0 +1,2114 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/4530 + gateway.networking.k8s.io/bundle-version: v1.5.0 + gateway.networking.k8s.io/channel: standard + name: tlsroutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: TLSRoute + listKind: TLSRouteList + plural: tlsroutes + singular: tlsroute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + The TLSRoute resource is similar to TCPRoute, but can be configured + to match against TLS-specific metadata. This allows more flexibility + in matching streams for a given TLS listener. + + If you need to forward traffic to a single target for a TLS listener, you + could choose to use a TCPRoute with a TLS listener. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of TLSRoute. + properties: + hostnames: + description: |- + Hostnames defines a set of SNI hostnames that should match against the + SNI attribute of TLS ClientHello message in TLS handshake. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed in SNI hostnames per RFC 6066. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: Hostnames cannot contain an IP + rule: self.all(h, !isIP(h)) + - message: Hostnames must be valid based on RFC-1123 + rule: 'self.all(h, !h.contains(''*'') ? h.matches(''^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$'') + : true)' + - message: Wildcards on hostnames must be the first label, and the + rest of hostname must be valid based on RFC-1123 + rule: 'self.all(h, h.contains(''*'') ? (h.startsWith(''*.'') && + h.substring(2).matches(''^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$'')) + : true)' + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + description: Rules are a list of actions. + items: + description: TLSRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. If unspecified or invalid (refers to a nonexistent resource or + a Service with no endpoints), the rule performs no forwarding; if no + filters are specified that would result in a response being sent, the + underlying implementation must actively reject request attempts to this + backend, by rejecting the connection. Request rejections must respect + weight; if an invalid backend is requested to have 80% of requests, then + 80% of requests must be rejected instead. + + Support: Core for Kubernetes Service + + Support: Extended for Kubernetes ServiceImport + + Support: Implementation-specific for any other resource + + Support for weight: Extended + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + name: + description: Name is the name of the route rule. This name MUST + be unique within a Route if it is set. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - backendRefs + type: object + maxItems: 1 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - hostnames + - rules + type: object + status: + description: Status defines the current state of TLSRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: The v1alpha2 version of TLSRoute has been deprecated and will + be removed in a future release of the API. Please upgrade to v1. + name: v1alpha2 + schema: + openAPIV3Schema: + description: |- + The TLSRoute resource is similar to TCPRoute, but can be configured + to match against TLS-specific metadata. This allows more flexibility + in matching streams for a given TLS listener. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of TLSRoute. + properties: + hostnames: + description: |- + Hostnames defines a set of SNI names that should match against the + SNI attribute of TLS ClientHello message in TLS handshake. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed in SNI names per RFC 6066. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + If a hostname is specified by both the Listener and TLSRoute, there + must be at least one intersecting hostname for the TLSRoute to be + attached to the Listener. For example: + + * A Listener with `test.example.com` as the hostname matches TLSRoutes + that have either not specified any hostnames, or have specified at + least one of `test.example.com` or `*.example.com`. + * A Listener with `*.example.com` as the hostname matches TLSRoutes + that have either not specified any hostnames or have specified at least + one hostname that matches the Listener hostname. For example, + `test.example.com` and `*.example.com` would both match. On the other + hand, `example.com` and `test.example.net` would not match. + + If both the Listener and TLSRoute have specified hostnames, any + TLSRoute hostnames that do not match the Listener hostname MUST be + ignored. For example, if a Listener specified `*.example.com`, and the + TLSRoute specified `test.example.com` and `test.example.net`, + `test.example.net` must not be considered for a match. + + If both the Listener and TLSRoute have specified hostnames, and none + match with the criteria above, then the TLSRoute is not accepted. The + implementation must raise an 'Accepted' Condition with a status of + `False` in the corresponding RouteParentStatus. + + Support: Core + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + x-kubernetes-list-type: atomic + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + description: Rules are a list of TLS matchers and actions. + items: + description: TLSRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. If unspecified or invalid (refers to a nonexistent resource or + a Service with no endpoints), the rule performs no forwarding; if no + filters are specified that would result in a response being sent, the + underlying implementation must actively reject request attempts to this + backend, by rejecting the connection. Request rejections must respect + weight; if an invalid backend is requested to have 80% of requests, then + 80% of requests must be rejected instead. + + Support: Core for Kubernetes Service + + Support: Extended for Kubernetes ServiceImport + + Support: Implementation-specific for any other resource + + Support for weight: Extended + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + name: + description: Name is the name of the route rule. This name MUST + be unique within a Route if it is set. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - backendRefs + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + status: + description: Status defines the current state of TLSRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: false + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: The v1alpha3 version of TLSRoute has been deprecated and will + be removed in a future release of the API. Please upgrade to v1. + name: v1alpha3 + schema: + openAPIV3Schema: + description: |- + The TLSRoute resource is similar to TCPRoute, but can be configured + to match against TLS-specific metadata. This allows more flexibility + in matching streams for a given TLS listener. + + If you need to forward traffic to a single target for a TLS listener, you + could choose to use a TCPRoute with a TLS listener. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of TLSRoute. + properties: + hostnames: + description: |- + Hostnames defines a set of SNI hostnames that should match against the + SNI attribute of TLS ClientHello message in TLS handshake. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed in SNI hostnames per RFC 6066. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + items: + description: |- + Hostname is the fully qualified domain name of a network host. This matches + the RFC 1123 definition of a hostname with 2 notable exceptions: + + 1. IPs are not allowed. + 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + + Hostname can be "precise" which is a domain name without the terminating + dot of a network host (e.g. "foo.example.com") or "wildcard", which is a + domain name prefixed with a single wildcard label (e.g. `*.example.com`). + + Note that as per RFC1035 and RFC1123, a *label* must consist of lower case + alphanumeric characters or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed. + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: Hostnames cannot contain an IP + rule: self.all(h, !isIP(h)) + - message: Hostnames must be valid based on RFC-1123 + rule: 'self.all(h, !h.contains(''*'') ? h.matches(''^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$'') + : true)' + - message: Wildcards on hostnames must be the first label, and the + rest of hostname must be valid based on RFC-1123 + rule: 'self.all(h, h.contains(''*'') ? (h.startsWith(''*.'') && + h.substring(2).matches(''^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)$'')) + : true)' + parentRefs: + description: |- + ParentRefs references the resources (usually Gateways) that a Route wants + to be attached to. Note that the referenced parent resource needs to + allow this for the attachment to be complete. For Gateways, that means + the Gateway needs to allow attachment from Routes of this kind and + namespace. For Services, that means the Service must either be in the same + namespace for a "producer" route, or the mesh implementation must support + and allow "consumer" routes for the referenced Service. ReferenceGrant is + not applicable for governing ParentRefs to Services - it is not possible to + create a "producer" route for a Service in a different namespace from the + Route. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + ParentRefs must be _distinct_. This means either that: + + * They select different objects. If this is the case, then parentRef + entries are distinct. In terms of fields, this means that the + multi-part key defined by `group`, `kind`, `namespace`, and `name` must + be unique across all parentRef entries in the Route. + * They do not select different objects, but for each optional field used, + each ParentRef that selects the same object must set the same set of + optional fields to different values. If one ParentRef sets a + combination of optional fields, all must set the same combination. + + Some examples: + + * If one ParentRef sets `sectionName`, all ParentRefs referencing the + same object must also set `sectionName`. + * If one ParentRef sets `port`, all ParentRefs referencing the same + object must also set `port`. + * If one ParentRef sets `sectionName` and `port`, all ParentRefs + referencing the same object must also set `sectionName` and `port`. + + It is possible to separately reference multiple distinct objects that may + be collapsed by an implementation. For example, some implementations may + choose to merge compatible Gateway Listeners together. If that is the + case, the list of routes attached to those resources should also be + merged. + + Note that for ParentRefs that cross namespace boundaries, there are specific + rules. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example, + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable other kinds of cross-namespace reference. + items: + description: |- + ParentReference identifies an API object (usually a Gateway) that can be considered + a parent of this resource (usually a route). There are two kinds of parent resources + with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + This API may be extended in the future to support additional kinds of parent + resources. + + The API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + x-kubernetes-validations: + - message: sectionName must be specified when parentRefs includes + 2 or more references to the same parent + rule: 'self.all(p1, self.all(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '''') && (!has(p2.__namespace__) || p2.__namespace__ + == '''')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) ? ((!has(p1.sectionName) + || p1.sectionName == '''') == (!has(p2.sectionName) || p2.sectionName + == '''')) : true))' + - message: sectionName must be unique when parentRefs includes 2 or + more references to the same parent + rule: self.all(p1, self.exists_one(p2, p1.group == p2.group && p1.kind + == p2.kind && p1.name == p2.name && (((!has(p1.__namespace__) + || p1.__namespace__ == '') && (!has(p2.__namespace__) || p2.__namespace__ + == '')) || (has(p1.__namespace__) && has(p2.__namespace__) && + p1.__namespace__ == p2.__namespace__ )) && (((!has(p1.sectionName) + || p1.sectionName == '') && (!has(p2.sectionName) || p2.sectionName + == '')) || (has(p1.sectionName) && has(p2.sectionName) && p1.sectionName + == p2.sectionName)))) + rules: + description: Rules are a list of actions. + items: + description: TLSRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: |- + BackendRefs defines the backend(s) where matching requests should be + sent. If unspecified or invalid (refers to a nonexistent resource or + a Service with no endpoints), the rule performs no forwarding; if no + filters are specified that would result in a response being sent, the + underlying implementation must actively reject request attempts to this + backend, by rejecting the connection. Request rejections must respect + weight; if an invalid backend is requested to have 80% of requests, then + 80% of requests must be rejected instead. + + Support: Core for Kubernetes Service + + Support: Extended for Kubernetes ServiceImport + + Support: Implementation-specific for any other resource + + Support for weight: Extended + items: + description: |- + BackendRef defines how a Route should forward a request to a Kubernetes + resource. + + Note that when a namespace different than the local namespace is specified, a + ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Note that when the BackendTLSPolicy object is enabled by the implementation, + there are some extra rules about validity to consider here. See the fields + where this struct is used for more information about the exact behavior. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: |- + Weight specifies the proportion of requests forwarded to the referenced + backend. This is computed as weight/(sum of all weights in this + BackendRefs list). For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision an + implementation supports. Weight is not a percentage and the sum of + weights does not need to equal 100. + + If only one backend is specified and it has a weight greater than 0, 100% + of the traffic is forwarded to that backend. If weight is set to 0, no + traffic should be forwarded for this entry. If unspecified, weight + defaults to 1. + + Support for this field varies based on the context where used. + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + name: + description: Name is the name of the route rule. This name MUST + be unique within a Route if it is set. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - backendRefs + type: object + maxItems: 1 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - hostnames + - rules + type: object + status: + description: Status defines the current state of TLSRoute. + properties: + parents: + description: |- + Parents is a list of parent resources (usually Gateways) that are + associated with the route, and the status of the route with respect to + each parent. When this route attaches to a parent, the controller that + manages the parent must add an entry to this list when the controller + first sees the route and should update the entry as appropriate when the + route or gateway is modified. + + Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this API + can only populate Route status for the Gateways/parent resources they are + responsible for. + + A maximum of 32 Gateways will be represented in this list. An empty list + means the route has not been attached to any Gateway. + items: + description: |- + RouteParentStatus describes the status of a route with respect to an + associated Parent. + properties: + conditions: + description: |- + Conditions describes the status of the route with respect to the Gateway. + Note that the route's availability is also subject to the Gateway's own + status conditions and listener status. + + If the Route's ParentRef specifies an existing Gateway that supports + Routes of this kind AND that Gateway's controller has sufficient access, + then that Gateway's controller MUST set the "Accepted" condition on the + Route, to indicate whether the route has been accepted or rejected by the + Gateway, and why. + + A Route MUST be considered "Accepted" if at least one of the Route's + rules is implemented by the Gateway. + + There are a number of cases where the "Accepted" condition may not be set + due to lack of controller visibility, that includes when: + + * The Route refers to a nonexistent parent. + * The Route is of a type that the controller does not support. + * The Route is in a namespace to which the controller does not have access. + items: + description: Condition contains details for one aspect of + the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + Example: "example.net/gateway-controller". + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: |- + ParentRef corresponds with a ParentRef in the spec that this + RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + There are two kinds of parent resources with "Core" support: + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - conditions + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + required: + - parents + type: object + required: + - spec + type: object + served: false + storage: false + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_vap_safeupgrades.yaml b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_vap_safeupgrades.yaml new file mode 100644 index 00000000..e2c8c8a4 --- /dev/null +++ b/pkg/vendored/gateway-api/crds/gateway.networking.k8s.io_vap_safeupgrades.yaml @@ -0,0 +1,48 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + annotations: + gateway.networking.k8s.io/bundle-version: v1.5.0-dev + gateway.networking.k8s.io/channel: standard + name: "safe-upgrades.gateway.networking.k8s.io" +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["apiextensions.k8s.io"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["*"] + validations: + - expression: "object.spec.group != 'gateway.networking.k8s.io' || oldObject == null || ( + has(object.metadata.annotations) && object.metadata.annotations.exists(k, k == 'gateway.networking.k8s.io/channel') && + object.metadata.annotations['gateway.networking.k8s.io/channel'] == 'standard' ) || ( + oldObject != null && has(oldObject.metadata.annotations) && oldObject.metadata.annotations.exists(k, k == 'gateway.networking.k8s.io/channel') && + oldObject.metadata.annotations['gateway.networking.k8s.io/channel'] == 'experimental' )" + message: "Installing experimental CRDs on top of standard channel CRDs is prohibited by default. Uninstall ValidatingAdmissionPolicy safe-upgrades.gateway.networking.k8s.io to install experimental CRDs on top of standard channel CRDs." + reason: Invalid + - expression: "object.spec.group != 'gateway.networking.k8s.io' || + (has(object.metadata.annotations) && object.metadata.annotations.exists(k, k == 'gateway.networking.k8s.io/bundle-version') && + !matches(object.metadata.annotations['gateway.networking.k8s.io/bundle-version'], 'v1.[0-4].\\\\d+') && + !matches(object.metadata.annotations['gateway.networking.k8s.io/bundle-version'], 'v0'))" #TODO Kubernetes 1.37: Migrate to kubernetes semver library + message: "Installing CRDs with version before v1.5.0 is prohibited by default. Uninstall ValidatingAdmissionPolicy safe-upgrades.gateway.networking.k8s.io to install older versions." + reason: Invalid + +--- + +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + annotations: + gateway.networking.k8s.io/bundle-version: v1.5.0-dev + gateway.networking.k8s.io/channel: standard + name: safe-upgrades.gateway.networking.k8s.io +spec: + policyName: safe-upgrades.gateway.networking.k8s.io + validationActions: [Deny] + matchResources: + resourceRules: + - apiGroups: ["apiextensions.k8s.io"] + apiVersions: ["v1"] + resources: ["customresourcedefinitions"] + operations: ["CREATE", "UPDATE"] diff --git a/pkg/vendored/moco/crds/moco_crds.yaml b/pkg/vendored/moco/crds/moco_crds.yaml new file mode 100644 index 00000000..d485f15b --- /dev/null +++ b/pkg/vendored/moco/crds/moco_crds.yaml @@ -0,0 +1,8620 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + helm.sh/resource-policy: keep + labels: + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: moco + app.kubernetes.io/version: vendored + helm.sh/chart: moco-vendored + name: backuppolicies.moco.cybozu.com +spec: + group: moco.cybozu.com + names: + kind: BackupPolicy + listKind: BackupPolicyList + plural: backuppolicies + singular: backuppolicy + scope: Namespaced + versions: + - name: v1beta2 + schema: + openAPIV3Schema: + description: BackupPolicy is a namespaced resource that should... + properties: + apiVersion: + description: APIVersion defines the versioned schema of this... + type: string + kind: + description: Kind is a string value representing the REST... + type: string + metadata: + type: object + spec: + description: BackupPolicySpec defines the configuration items... + properties: + activeDeadlineSeconds: + description: Specifies the duration in seconds relative to the... + format: int64 + nullable: true + type: integer + backoffLimit: + description: Specifies the number of retries before marking... + format: int32 + minimum: 0 + nullable: true + type: integer + concurrencyPolicy: + default: Allow + description: Specifies how to treat concurrent executions of a... + enum: + - Allow + - Forbid + - Replace + type: string + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. + format: int32 + minimum: 0 + nullable: true + type: integer + jobConfig: + description: Specifies parameters for backup Pod. + properties: + affinity: + description: If specified, the pod's scheduling constraints. + properties: + nodeAffinity: + description: NodeAffinityApplyConfiguration represents a... + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + description: PreferredSchedulingTermApplyConfiguration... + properties: + preference: + description: NodeSelectorTermApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: NodeSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A node selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchFields: + items: + description: NodeSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A node selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + type: object + weight: + format: int32 + type: integer + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: NodeSelectorApplyConfiguration represents a... + properties: + nodeSelectorTerms: + items: + description: NodeSelectorTermApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: NodeSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A node selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchFields: + items: + description: NodeSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A node selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + type: object + type: array + type: object + type: object + podAffinity: + description: PodAffinityApplyConfiguration represents a... + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + description: WeightedPodAffinityTermApplyConfiguration... + properties: + podAffinityTerm: + description: PodAffinityTermApplyConfiguration represents a... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + matchLabelKeys: + items: + type: string + type: array + mismatchLabelKeys: + items: + type: string + type: array + namespaceSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + namespaces: + items: + type: string + type: array + topologyKey: + type: string + type: object + weight: + format: int32 + type: integer + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + description: PodAffinityTermApplyConfiguration represents a... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + matchLabelKeys: + items: + type: string + type: array + mismatchLabelKeys: + items: + type: string + type: array + namespaceSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + namespaces: + items: + type: string + type: array + topologyKey: + type: string + type: object + type: array + type: object + podAntiAffinity: + description: PodAntiAffinityApplyConfiguration represents a... + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + description: WeightedPodAffinityTermApplyConfiguration... + properties: + podAffinityTerm: + description: PodAffinityTermApplyConfiguration represents a... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + matchLabelKeys: + items: + type: string + type: array + mismatchLabelKeys: + items: + type: string + type: array + namespaceSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + namespaces: + items: + type: string + type: array + topologyKey: + type: string + type: object + weight: + format: int32 + type: integer + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + description: PodAffinityTermApplyConfiguration represents a... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + matchLabelKeys: + items: + type: string + type: array + mismatchLabelKeys: + items: + type: string + type: array + namespaceSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + namespaces: + items: + type: string + type: array + topologyKey: + type: string + type: object + type: array + type: object + type: object + bucketConfig: + description: Specifies how to access an object storage bucket. + properties: + backendType: + default: s3 + description: BackendType is an identifier for the object... + enum: + - s3 + - gcs + type: string + bucketName: + description: The name of the bucket + minLength: 1 + type: string + caCert: + description: Path to SSL CA certificate file used in addition... + type: string + endpointURL: + description: The API endpoint URL. + pattern: ^https?://.* + type: string + region: + description: The region of the bucket. + type: string + usePathStyle: + description: Allows you to enable the client to use path-style... + type: boolean + required: + - bucketName + type: object + cpu: + anyOf: + - type: integer + - type: string + default: 4 + description: CPU is the amount of CPU requested for the Pod. + nullable: true + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + env: + description: List of environment variables to set in the... + items: + description: EnvVarApplyConfiguration is the type defined to... + properties: + name: + type: string + value: + type: string + valueFrom: + description: EnvVarSourceApplyConfiguration represents a... + properties: + configMapKeyRef: + description: ConfigMapKeySelectorApplyConfiguration represents... + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + fileKeyRef: + description: FileKeySelectorApplyConfiguration represents a... + properties: + key: + type: string + optional: + type: boolean + path: + type: string + volumeName: + type: string + type: object + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + secretKeyRef: + description: SecretKeySelectorApplyConfiguration represents a... + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + type: object + type: object + type: array + envFrom: + description: List of sources to populate environment variables... + items: + description: EnvFromSourceApplyConfiguration is the type... + properties: + configMapRef: + description: ConfigMapEnvSourceApplyConfiguration represents a... + properties: + name: + type: string + optional: + type: boolean + type: object + prefix: + type: string + secretRef: + description: SecretEnvSourceApplyConfiguration represents a... + properties: + name: + type: string + optional: + type: boolean + type: object + type: object + type: array + maxCpu: + anyOf: + - type: integer + - type: string + description: MaxCPU is the amount of maximum CPU for the Pod. + nullable: true + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + maxMemory: + anyOf: + - type: integer + - type: string + description: MaxMemory is the amount of maximum memory for the... + nullable: true + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + memory: + anyOf: + - type: integer + - type: string + default: 4Gi + description: Memory is the amount of memory requested for the... + nullable: true + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + serviceAccountName: + description: ServiceAccountName specifies the ServiceAccount... + minLength: 1 + type: string + threads: + default: 4 + description: Threads is the number of threads used for backup... + minimum: 1 + type: integer + volumeMounts: + description: VolumeMounts describes a list of volume mounts... + items: + description: VolumeMountApplyConfiguration is the type defined... + properties: + mountPath: + type: string + mountPropagation: + description: MountPropagationMode describes mount propagation. + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + description: RecursiveReadOnlyMode describes... + type: string + subPath: + type: string + subPathExpr: + type: string + type: object + type: array + volumes: + description: Volumes defines the list of volumes that can be... + items: + description: VolumeApplyConfiguration is the type defined to... + properties: + awsElasticBlockStore: + description: AWSElasticBlockStoreVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + type: object + azureDisk: + description: AzureDiskVolumeSourceApplyConfiguration... + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + type: object + azureFile: + description: AzureFileVolumeSourceApplyConfiguration... + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + type: object + cephfs: + description: CephFSVolumeSourceApplyConfiguration represents a... + properties: + monitors: + items: + type: string + type: array + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + user: + type: string + type: object + cinder: + description: CinderVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + volumeID: + type: string + type: object + configMap: + description: ConfigMapVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + csi: + description: CSIVolumeSourceApplyConfiguration represents a... + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + type: object + downwardAPI: + description: DownwardAPIVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: DownwardAPIVolumeFileApplyConfiguration... + properties: + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + type: object + type: array + type: object + emptyDir: + description: EmptyDirVolumeSourceApplyConfiguration represents... + properties: + medium: + description: StorageMedium defines ways that storage can be... + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: EphemeralVolumeSourceApplyConfiguration... + properties: + volumeClaimTemplate: + description: PersistentVolumeClaimTemplateApplyConfiguration... + properties: + metadata: + description: ObjectMetaApplyConfiguration represents a... + properties: + annotations: + additionalProperties: + type: string + type: object + creationTimestamp: + format: date-time + type: string + deletionGracePeriodSeconds: + format: int64 + type: integer + deletionTimestamp: + format: date-time + type: string + finalizers: + items: + type: string + type: array + generateName: + type: string + generation: + format: int64 + type: integer + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + ownerReferences: + items: + description: OwnerReferenceApplyConfiguration represents a... + properties: + apiVersion: + type: string + blockOwnerDeletion: + type: boolean + controller: + type: boolean + kind: + type: string + name: + type: string + uid: + description: UID is a type that holds unique ID values,... + type: string + type: object + type: array + resourceVersion: + type: string + uid: + description: UID is a type that holds unique ID values,... + type: string + type: object + spec: + description: PersistentVolumeClaimSpecApplyConfiguration... + properties: + accessModes: + items: + type: string + type: array + dataSource: + description: TypedLocalObjectReferenceApplyConfiguration... + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + type: object + dataSourceRef: + description: TypedObjectReferenceApplyConfiguration represents... + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + type: object + resources: + description: VolumeResourceRequirementsApplyConfiguration... + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + type: object + selector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + description: PersistentVolumeMode describes how a volume is... + type: string + volumeName: + type: string + type: object + type: object + type: object + fc: + description: FCVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + wwids: + items: + type: string + type: array + type: object + flexVolume: + description: FlexVolumeSourceApplyConfiguration represents a... + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + type: object + flocker: + description: FlockerVolumeSourceApplyConfiguration represents... + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + description: GCEPersistentDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + type: object + gitRepo: + description: GitRepoVolumeSourceApplyConfiguration represents... + properties: + directory: + type: string + repository: + type: string + revision: + type: string + type: object + glusterfs: + description: GlusterfsVolumeSourceApplyConfiguration... + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + type: object + hostPath: + description: HostPathVolumeSourceApplyConfiguration represents... + properties: + path: + type: string + type: + type: string + type: object + image: + description: ImageVolumeSourceApplyConfiguration represents a... + properties: + pullPolicy: + description: PullPolicy describes a policy for if/when to pull... + type: string + reference: + type: string + type: object + iscsi: + description: ISCSIVolumeSourceApplyConfiguration represents a... + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + targetPortal: + type: string + type: object + name: + type: string + nfs: + description: NFSVolumeSourceApplyConfiguration represents a... + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + type: object + persistentVolumeClaim: + description: PersistentVolumeClaimVolumeSourceApplyConfiguratio... + properties: + claimName: + type: string + readOnly: + type: boolean + type: object + photonPersistentDisk: + description: PhotonPersistentDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + pdID: + type: string + type: object + portworxVolume: + description: PortworxVolumeSourceApplyConfiguration represents... + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + type: object + projected: + description: ProjectedVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + description: VolumeProjectionApplyConfiguration represents a... + properties: + clusterTrustBundle: + description: ClusterTrustBundleProjectionApplyConfiguration... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + type: object + configMap: + description: ConfigMapProjectionApplyConfiguration represents... + properties: + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + downwardAPI: + description: DownwardAPIProjectionApplyConfiguration... + properties: + items: + items: + description: DownwardAPIVolumeFileApplyConfiguration... + properties: + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + type: object + type: array + type: object + podCertificate: + description: PodCertificateProjectionApplyConfiguration... + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + type: object + secret: + description: SecretProjectionApplyConfiguration represents a... + properties: + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + serviceAccountToken: + description: ServiceAccountTokenProjectionApplyConfiguration... + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + type: object + type: object + type: array + type: object + quobyte: + description: QuobyteVolumeSourceApplyConfiguration represents... + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + type: object + rbd: + description: RBDVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + pool: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + user: + type: string + type: object + scaleIO: + description: ScaleIOVolumeSourceApplyConfiguration represents... + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + type: object + secret: + description: SecretVolumeSourceApplyConfiguration represents a... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + optional: + type: boolean + secretName: + type: string + type: object + storageos: + description: StorageOSVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + description: VsphereVirtualDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + type: object + type: object + type: array + workVolume: + description: WorkVolume is the volume source for the working... + properties: + awsElasticBlockStore: + description: AWSElasticBlockStoreVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + type: object + azureDisk: + description: AzureDiskVolumeSourceApplyConfiguration... + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + type: object + azureFile: + description: AzureFileVolumeSourceApplyConfiguration... + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + type: object + cephfs: + description: CephFSVolumeSourceApplyConfiguration represents a... + properties: + monitors: + items: + type: string + type: array + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + user: + type: string + type: object + cinder: + description: CinderVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + volumeID: + type: string + type: object + configMap: + description: ConfigMapVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + csi: + description: CSIVolumeSourceApplyConfiguration represents a... + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + type: object + downwardAPI: + description: DownwardAPIVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: DownwardAPIVolumeFileApplyConfiguration... + properties: + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + type: object + type: array + type: object + emptyDir: + description: EmptyDirVolumeSourceApplyConfiguration represents... + properties: + medium: + description: StorageMedium defines ways that storage can be... + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: EphemeralVolumeSourceApplyConfiguration... + properties: + volumeClaimTemplate: + description: PersistentVolumeClaimTemplateApplyConfiguration... + properties: + metadata: + description: ObjectMetaApplyConfiguration represents a... + properties: + annotations: + additionalProperties: + type: string + type: object + creationTimestamp: + format: date-time + type: string + deletionGracePeriodSeconds: + format: int64 + type: integer + deletionTimestamp: + format: date-time + type: string + finalizers: + items: + type: string + type: array + generateName: + type: string + generation: + format: int64 + type: integer + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + ownerReferences: + items: + description: OwnerReferenceApplyConfiguration represents a... + properties: + apiVersion: + type: string + blockOwnerDeletion: + type: boolean + controller: + type: boolean + kind: + type: string + name: + type: string + uid: + description: UID is a type that holds unique ID values,... + type: string + type: object + type: array + resourceVersion: + type: string + uid: + description: UID is a type that holds unique ID values,... + type: string + type: object + spec: + description: PersistentVolumeClaimSpecApplyConfiguration... + properties: + accessModes: + items: + type: string + type: array + dataSource: + description: TypedLocalObjectReferenceApplyConfiguration... + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + type: object + dataSourceRef: + description: TypedObjectReferenceApplyConfiguration represents... + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + type: object + resources: + description: VolumeResourceRequirementsApplyConfiguration... + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + type: object + selector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + description: PersistentVolumeMode describes how a volume is... + type: string + volumeName: + type: string + type: object + type: object + type: object + fc: + description: FCVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + wwids: + items: + type: string + type: array + type: object + flexVolume: + description: FlexVolumeSourceApplyConfiguration represents a... + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + type: object + flocker: + description: FlockerVolumeSourceApplyConfiguration represents... + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + description: GCEPersistentDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + type: object + gitRepo: + description: GitRepoVolumeSourceApplyConfiguration represents... + properties: + directory: + type: string + repository: + type: string + revision: + type: string + type: object + glusterfs: + description: GlusterfsVolumeSourceApplyConfiguration... + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + type: object + hostPath: + description: HostPathVolumeSourceApplyConfiguration represents... + properties: + path: + type: string + type: + type: string + type: object + image: + description: ImageVolumeSourceApplyConfiguration represents a... + properties: + pullPolicy: + description: PullPolicy describes a policy for if/when to pull... + type: string + reference: + type: string + type: object + iscsi: + description: ISCSIVolumeSourceApplyConfiguration represents a... + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + targetPortal: + type: string + type: object + nfs: + description: NFSVolumeSourceApplyConfiguration represents a... + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + type: object + persistentVolumeClaim: + description: PersistentVolumeClaimVolumeSourceApplyConfiguratio... + properties: + claimName: + type: string + readOnly: + type: boolean + type: object + photonPersistentDisk: + description: PhotonPersistentDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + pdID: + type: string + type: object + portworxVolume: + description: PortworxVolumeSourceApplyConfiguration represents... + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + type: object + projected: + description: ProjectedVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + description: VolumeProjectionApplyConfiguration represents a... + properties: + clusterTrustBundle: + description: ClusterTrustBundleProjectionApplyConfiguration... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + type: object + configMap: + description: ConfigMapProjectionApplyConfiguration represents... + properties: + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + downwardAPI: + description: DownwardAPIProjectionApplyConfiguration... + properties: + items: + items: + description: DownwardAPIVolumeFileApplyConfiguration... + properties: + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + type: object + type: array + type: object + podCertificate: + description: PodCertificateProjectionApplyConfiguration... + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + type: object + secret: + description: SecretProjectionApplyConfiguration represents a... + properties: + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + serviceAccountToken: + description: ServiceAccountTokenProjectionApplyConfiguration... + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + type: object + type: object + type: array + type: object + quobyte: + description: QuobyteVolumeSourceApplyConfiguration represents... + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + type: object + rbd: + description: RBDVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + pool: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + user: + type: string + type: object + scaleIO: + description: ScaleIOVolumeSourceApplyConfiguration represents... + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + type: object + secret: + description: SecretVolumeSourceApplyConfiguration represents a... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + optional: + type: boolean + secretName: + type: string + type: object + storageos: + description: StorageOSVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + description: VsphereVirtualDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + type: object + type: object + required: + - bucketConfig + - serviceAccountName + - workVolume + type: object + schedule: + description: The schedule in Cron format for periodic backups. + type: string + startingDeadlineSeconds: + description: Optional deadline in seconds for starting the job... + format: int64 + nullable: true + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + format: int32 + minimum: 0 + nullable: true + type: integer + required: + - jobConfig + - schedule + type: object + required: + - spec + type: object + served: true + storage: true +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: moco-system/moco-serving-cert + controller-gen.kubebuilder.io/version: v0.19.0 + helm.sh/resource-policy: keep + labels: + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: moco + app.kubernetes.io/version: vendored + helm.sh/chart: moco-vendored + name: mysqlclusters.moco.cybozu.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + name: moco-webhook-service + namespace: moco-system + path: /convert + conversionReviewVersions: + - v1 + group: moco.cybozu.com + names: + kind: MySQLCluster + listKind: MySQLClusterList + plural: mysqlclusters + singular: mysqlcluster + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string + - jsonPath: .status.conditions[?(@.type=='Healthy')].status + name: Healthy + type: string + - jsonPath: .status.currentPrimaryIndex + name: Primary + type: integer + - jsonPath: .status.syncedReplicas + name: Synced replicas + type: integer + - jsonPath: .status.errantReplicas + name: Errant replicas + type: integer + - jsonPath: .status.conditions[?(@.type=='ClusteringActive')].status + name: Clustering Active + type: string + - jsonPath: .status.conditions[?(@.type=='ReconciliationActive')].status + name: Reconcile Active + type: string + - jsonPath: .status.backup.time + name: Last backup + type: string + name: v1beta2 + schema: + openAPIV3Schema: + description: MySQLCluster is the Schema for the mysqlclusters... + properties: + apiVersion: + description: APIVersion defines the versioned schema of this... + type: string + kind: + description: Kind is a string value representing the REST... + type: string + metadata: + type: object + spec: + description: MySQLClusterSpec defines the desired state of... + properties: + agentUseLocalhost: + description: AgentUseLocalhost configures the mysqld interface... + type: boolean + backupPolicyName: + description: The name of BackupPolicy custom resource in the... + nullable: true + type: string + collectors: + description: Collectors is the list of collector flag names of... + items: + type: string + type: array + disableSlowQueryLogContainer: + description: DisableSlowQueryLogContainer controls whether to... + type: boolean + initializeTimezoneData: + default: false + description: InitializeTimezoneData controls whether the init... + type: boolean + logRotationSchedule: + description: LogRotationSchedule specifies the schedule to... + type: string + logRotationSize: + description: LogRotationSize specifies the size to rotate... + type: integer + maxDelaySeconds: + default: 60 + description: MaxDelaySeconds configures the readiness probe of... + minimum: 0 + type: integer + maxDelaySecondsForPodDeletion: + default: 0 + description: MaxDelaySecondsForPodDeletion configures the... + format: int64 + minimum: 0 + type: integer + mysqlConfigMapName: + description: MySQLConfigMapName is a `ConfigMap` name of MySQL... + nullable: true + type: string + offline: + description: Offline sets the cluster offline, releasing... + type: boolean + podTemplate: + description: PodTemplate is a `Pod` template for MySQL server... + properties: + metadata: + description: Standard object's metadata. + properties: + annotations: + additionalProperties: + type: string + description: Annotations is a map of string keys and values. + type: object + labels: + additionalProperties: + type: string + description: Labels is a map of string keys and values. + type: object + name: + description: Name is the name of the object. + type: string + type: object + overwriteContainers: + description: OverwriteContainers overwrites the container... + items: + description: OverwriteContainer defines the container spec... + properties: + name: + description: Name of the container to overwrite. + enum: + - agent + - moco-init + - slow-log + - mysqld-exporter + type: string + resources: + description: Resources is the container resource to be... + properties: + claims: + items: + description: ResourceClaimApplyConfiguration represents a... + properties: + name: + type: string + request: + type: string + type: object + type: array + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + type: object + securityContext: + description: SecurityContext is the container SecurityContext... + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + description: AppArmorProfileApplyConfiguration represents a... + properties: + localhostProfile: + type: string + type: + type: string + type: object + capabilities: + description: CapabilitiesApplyConfiguration represents a... + properties: + add: + items: + description: Capability represent POSIX capabilities type + type: string + type: array + drop: + items: + description: Capability represent POSIX capabilities type + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + description: SELinuxOptionsApplyConfiguration represents a... + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + description: SeccompProfileApplyConfiguration represents a... + properties: + localhostProfile: + type: string + type: + description: SeccompProfileType defines the supported seccomp... + type: string + type: object + windowsOptions: + description: WindowsSecurityContextOptionsApplyConfiguration... + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + required: + - name + type: object + type: array + spec: + description: Specification of the desired behavior of the pod. + properties: + activeDeadlineSeconds: + format: int64 + type: integer + affinity: + description: AffinityApplyConfiguration represents a... + properties: + nodeAffinity: + description: NodeAffinityApplyConfiguration represents a... + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + description: PreferredSchedulingTermApplyConfiguration... + properties: + preference: + description: NodeSelectorTermApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: NodeSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A node selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchFields: + items: + description: NodeSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A node selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + type: object + weight: + format: int32 + type: integer + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: NodeSelectorApplyConfiguration represents a... + properties: + nodeSelectorTerms: + items: + description: NodeSelectorTermApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: NodeSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A node selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchFields: + items: + description: NodeSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A node selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + type: object + type: array + type: object + type: object + podAffinity: + description: PodAffinityApplyConfiguration represents a... + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + description: WeightedPodAffinityTermApplyConfiguration... + properties: + podAffinityTerm: + description: PodAffinityTermApplyConfiguration represents a... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + matchLabelKeys: + items: + type: string + type: array + mismatchLabelKeys: + items: + type: string + type: array + namespaceSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + namespaces: + items: + type: string + type: array + topologyKey: + type: string + type: object + weight: + format: int32 + type: integer + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + description: PodAffinityTermApplyConfiguration represents a... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + matchLabelKeys: + items: + type: string + type: array + mismatchLabelKeys: + items: + type: string + type: array + namespaceSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + namespaces: + items: + type: string + type: array + topologyKey: + type: string + type: object + type: array + type: object + podAntiAffinity: + description: PodAntiAffinityApplyConfiguration represents a... + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + description: WeightedPodAffinityTermApplyConfiguration... + properties: + podAffinityTerm: + description: PodAffinityTermApplyConfiguration represents a... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + matchLabelKeys: + items: + type: string + type: array + mismatchLabelKeys: + items: + type: string + type: array + namespaceSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + namespaces: + items: + type: string + type: array + topologyKey: + type: string + type: object + weight: + format: int32 + type: integer + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + description: PodAffinityTermApplyConfiguration represents a... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + matchLabelKeys: + items: + type: string + type: array + mismatchLabelKeys: + items: + type: string + type: array + namespaceSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + namespaces: + items: + type: string + type: array + topologyKey: + type: string + type: object + type: array + type: object + type: object + automountServiceAccountToken: + type: boolean + containers: + items: + description: ContainerApplyConfiguration represents a... + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + description: EnvVarApplyConfiguration represents a declarative... + properties: + name: + type: string + value: + type: string + valueFrom: + description: EnvVarSourceApplyConfiguration represents a... + properties: + configMapKeyRef: + description: ConfigMapKeySelectorApplyConfiguration represents... + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + fileKeyRef: + description: FileKeySelectorApplyConfiguration represents a... + properties: + key: + type: string + optional: + type: boolean + path: + type: string + volumeName: + type: string + type: object + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + secretKeyRef: + description: SecretKeySelectorApplyConfiguration represents a... + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + type: object + type: object + type: array + envFrom: + items: + description: EnvFromSourceApplyConfiguration represents a... + properties: + configMapRef: + description: ConfigMapEnvSourceApplyConfiguration represents a... + properties: + name: + type: string + optional: + type: boolean + type: object + prefix: + type: string + secretRef: + description: SecretEnvSourceApplyConfiguration represents a... + properties: + name: + type: string + optional: + type: boolean + type: object + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull... + type: string + lifecycle: + description: LifecycleApplyConfiguration represents a... + properties: + postStart: + description: LifecycleHandlerApplyConfiguration represents a... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + sleep: + description: SleepActionApplyConfiguration represents a... + properties: + seconds: + format: int64 + type: integer + type: object + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + type: object + preStop: + description: LifecycleHandlerApplyConfiguration represents a... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + sleep: + description: SleepActionApplyConfiguration represents a... + properties: + seconds: + format: int64 + type: integer + type: object + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + type: object + stopSignal: + description: Signal defines the stop signal of containers + type: string + type: object + livenessProbe: + description: ProbeApplyConfiguration represents a declarative... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + description: GRPCActionApplyConfiguration represents a... + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + description: ContainerPortApplyConfiguration represents a... + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + description: Protocol defines network protocols supported for... + type: string + type: object + type: array + readinessProbe: + description: ProbeApplyConfiguration represents a declarative... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + description: GRPCActionApplyConfiguration represents a... + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + description: ContainerResizePolicyApplyConfiguration... + properties: + resourceName: + description: ResourceName is the name identifying various... + type: string + restartPolicy: + description: ResourceResizeRestartPolicy specifies how to... + type: string + type: object + type: array + resources: + description: ResourceRequirementsApplyConfiguration represents... + properties: + claims: + items: + description: ResourceClaimApplyConfiguration represents a... + properties: + name: + type: string + request: + type: string + type: object + type: array + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + type: object + restartPolicy: + description: ContainerRestartPolicy is the restart policy for... + type: string + restartPolicyRules: + items: + description: ContainerRestartRuleApplyConfiguration represents... + properties: + action: + description: ContainerRestartRuleAction describes the action... + type: string + exitCodes: + description: ContainerRestartRuleOnExitCodesApplyConfiguration... + properties: + operator: + description: ContainerRestartRuleOnExitCodesOperator describes... + type: string + values: + items: + format: int32 + type: integer + type: array + type: object + type: object + type: array + securityContext: + description: SecurityContextApplyConfiguration represents a... + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + description: AppArmorProfileApplyConfiguration represents a... + properties: + localhostProfile: + type: string + type: + type: string + type: object + capabilities: + description: CapabilitiesApplyConfiguration represents a... + properties: + add: + items: + description: Capability represent POSIX capabilities type + type: string + type: array + drop: + items: + description: Capability represent POSIX capabilities type + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + description: SELinuxOptionsApplyConfiguration represents a... + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + description: SeccompProfileApplyConfiguration represents a... + properties: + localhostProfile: + type: string + type: + description: SeccompProfileType defines the supported seccomp... + type: string + type: object + windowsOptions: + description: WindowsSecurityContextOptionsApplyConfiguration... + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + description: ProbeApplyConfiguration represents a declarative... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + description: GRPCActionApplyConfiguration represents a... + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + description: TerminationMessagePolicy describes how... + type: string + tty: + type: boolean + volumeDevices: + items: + description: VolumeDeviceApplyConfiguration represents a... + properties: + devicePath: + type: string + name: + type: string + type: object + type: array + volumeMounts: + items: + description: VolumeMountApplyConfiguration represents a... + properties: + mountPath: + type: string + mountPropagation: + description: MountPropagationMode describes mount propagation. + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + description: RecursiveReadOnlyMode describes... + type: string + subPath: + type: string + subPathExpr: + type: string + type: object + type: array + workingDir: + type: string + type: object + type: array + dnsConfig: + description: PodDNSConfigApplyConfiguration represents a... + properties: + nameservers: + items: + type: string + type: array + options: + items: + description: PodDNSConfigOptionApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + searches: + items: + type: string + type: array + type: object + dnsPolicy: + description: DNSPolicy defines how a pod's DNS will be... + type: string + enableServiceLinks: + type: boolean + ephemeralContainers: + items: + description: EphemeralContainerApplyConfiguration represents a... + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + description: EnvVarApplyConfiguration represents a declarative... + properties: + name: + type: string + value: + type: string + valueFrom: + description: EnvVarSourceApplyConfiguration represents a... + properties: + configMapKeyRef: + description: ConfigMapKeySelectorApplyConfiguration represents... + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + fileKeyRef: + description: FileKeySelectorApplyConfiguration represents a... + properties: + key: + type: string + optional: + type: boolean + path: + type: string + volumeName: + type: string + type: object + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + secretKeyRef: + description: SecretKeySelectorApplyConfiguration represents a... + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + type: object + type: object + type: array + envFrom: + items: + description: EnvFromSourceApplyConfiguration represents a... + properties: + configMapRef: + description: ConfigMapEnvSourceApplyConfiguration represents a... + properties: + name: + type: string + optional: + type: boolean + type: object + prefix: + type: string + secretRef: + description: SecretEnvSourceApplyConfiguration represents a... + properties: + name: + type: string + optional: + type: boolean + type: object + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull... + type: string + lifecycle: + description: LifecycleApplyConfiguration represents a... + properties: + postStart: + description: LifecycleHandlerApplyConfiguration represents a... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + sleep: + description: SleepActionApplyConfiguration represents a... + properties: + seconds: + format: int64 + type: integer + type: object + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + type: object + preStop: + description: LifecycleHandlerApplyConfiguration represents a... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + sleep: + description: SleepActionApplyConfiguration represents a... + properties: + seconds: + format: int64 + type: integer + type: object + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + type: object + stopSignal: + description: Signal defines the stop signal of containers + type: string + type: object + livenessProbe: + description: ProbeApplyConfiguration represents a declarative... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + description: GRPCActionApplyConfiguration represents a... + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + description: ContainerPortApplyConfiguration represents a... + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + description: Protocol defines network protocols supported for... + type: string + type: object + type: array + readinessProbe: + description: ProbeApplyConfiguration represents a declarative... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + description: GRPCActionApplyConfiguration represents a... + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + description: ContainerResizePolicyApplyConfiguration... + properties: + resourceName: + description: ResourceName is the name identifying various... + type: string + restartPolicy: + description: ResourceResizeRestartPolicy specifies how to... + type: string + type: object + type: array + resources: + description: ResourceRequirementsApplyConfiguration represents... + properties: + claims: + items: + description: ResourceClaimApplyConfiguration represents a... + properties: + name: + type: string + request: + type: string + type: object + type: array + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + type: object + restartPolicy: + description: ContainerRestartPolicy is the restart policy for... + type: string + restartPolicyRules: + items: + description: ContainerRestartRuleApplyConfiguration represents... + properties: + action: + description: ContainerRestartRuleAction describes the action... + type: string + exitCodes: + description: ContainerRestartRuleOnExitCodesApplyConfiguration... + properties: + operator: + description: ContainerRestartRuleOnExitCodesOperator describes... + type: string + values: + items: + format: int32 + type: integer + type: array + type: object + type: object + type: array + securityContext: + description: SecurityContextApplyConfiguration represents a... + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + description: AppArmorProfileApplyConfiguration represents a... + properties: + localhostProfile: + type: string + type: + type: string + type: object + capabilities: + description: CapabilitiesApplyConfiguration represents a... + properties: + add: + items: + description: Capability represent POSIX capabilities type + type: string + type: array + drop: + items: + description: Capability represent POSIX capabilities type + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + description: SELinuxOptionsApplyConfiguration represents a... + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + description: SeccompProfileApplyConfiguration represents a... + properties: + localhostProfile: + type: string + type: + description: SeccompProfileType defines the supported seccomp... + type: string + type: object + windowsOptions: + description: WindowsSecurityContextOptionsApplyConfiguration... + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + description: ProbeApplyConfiguration represents a declarative... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + description: GRPCActionApplyConfiguration represents a... + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + targetContainerName: + type: string + terminationMessagePath: + type: string + terminationMessagePolicy: + description: TerminationMessagePolicy describes how... + type: string + tty: + type: boolean + volumeDevices: + items: + description: VolumeDeviceApplyConfiguration represents a... + properties: + devicePath: + type: string + name: + type: string + type: object + type: array + volumeMounts: + items: + description: VolumeMountApplyConfiguration represents a... + properties: + mountPath: + type: string + mountPropagation: + description: MountPropagationMode describes mount propagation. + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + description: RecursiveReadOnlyMode describes... + type: string + subPath: + type: string + subPathExpr: + type: string + type: object + type: array + workingDir: + type: string + type: object + type: array + hostAliases: + items: + description: HostAliasApplyConfiguration represents a... + properties: + hostnames: + items: + type: string + type: array + ip: + type: string + type: object + type: array + hostIPC: + type: boolean + hostNetwork: + type: boolean + hostPID: + type: boolean + hostUsers: + type: boolean + hostname: + type: string + hostnameOverride: + type: string + imagePullSecrets: + items: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + type: array + initContainers: + items: + description: ContainerApplyConfiguration represents a... + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + env: + items: + description: EnvVarApplyConfiguration represents a declarative... + properties: + name: + type: string + value: + type: string + valueFrom: + description: EnvVarSourceApplyConfiguration represents a... + properties: + configMapKeyRef: + description: ConfigMapKeySelectorApplyConfiguration represents... + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + fileKeyRef: + description: FileKeySelectorApplyConfiguration represents a... + properties: + key: + type: string + optional: + type: boolean + path: + type: string + volumeName: + type: string + type: object + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + secretKeyRef: + description: SecretKeySelectorApplyConfiguration represents a... + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + type: object + type: object + type: array + envFrom: + items: + description: EnvFromSourceApplyConfiguration represents a... + properties: + configMapRef: + description: ConfigMapEnvSourceApplyConfiguration represents a... + properties: + name: + type: string + optional: + type: boolean + type: object + prefix: + type: string + secretRef: + description: SecretEnvSourceApplyConfiguration represents a... + properties: + name: + type: string + optional: + type: boolean + type: object + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull... + type: string + lifecycle: + description: LifecycleApplyConfiguration represents a... + properties: + postStart: + description: LifecycleHandlerApplyConfiguration represents a... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + sleep: + description: SleepActionApplyConfiguration represents a... + properties: + seconds: + format: int64 + type: integer + type: object + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + type: object + preStop: + description: LifecycleHandlerApplyConfiguration represents a... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + sleep: + description: SleepActionApplyConfiguration represents a... + properties: + seconds: + format: int64 + type: integer + type: object + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + type: object + stopSignal: + description: Signal defines the stop signal of containers + type: string + type: object + livenessProbe: + description: ProbeApplyConfiguration represents a declarative... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + description: GRPCActionApplyConfiguration represents a... + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + name: + type: string + ports: + items: + description: ContainerPortApplyConfiguration represents a... + properties: + containerPort: + format: int32 + type: integer + hostIP: + type: string + hostPort: + format: int32 + type: integer + name: + type: string + protocol: + description: Protocol defines network protocols supported for... + type: string + type: object + type: array + readinessProbe: + description: ProbeApplyConfiguration represents a declarative... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + description: GRPCActionApplyConfiguration represents a... + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + resizePolicy: + items: + description: ContainerResizePolicyApplyConfiguration... + properties: + resourceName: + description: ResourceName is the name identifying various... + type: string + restartPolicy: + description: ResourceResizeRestartPolicy specifies how to... + type: string + type: object + type: array + resources: + description: ResourceRequirementsApplyConfiguration represents... + properties: + claims: + items: + description: ResourceClaimApplyConfiguration represents a... + properties: + name: + type: string + request: + type: string + type: object + type: array + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + type: object + restartPolicy: + description: ContainerRestartPolicy is the restart policy for... + type: string + restartPolicyRules: + items: + description: ContainerRestartRuleApplyConfiguration represents... + properties: + action: + description: ContainerRestartRuleAction describes the action... + type: string + exitCodes: + description: ContainerRestartRuleOnExitCodesApplyConfiguration... + properties: + operator: + description: ContainerRestartRuleOnExitCodesOperator describes... + type: string + values: + items: + format: int32 + type: integer + type: array + type: object + type: object + type: array + securityContext: + description: SecurityContextApplyConfiguration represents a... + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + description: AppArmorProfileApplyConfiguration represents a... + properties: + localhostProfile: + type: string + type: + type: string + type: object + capabilities: + description: CapabilitiesApplyConfiguration represents a... + properties: + add: + items: + description: Capability represent POSIX capabilities type + type: string + type: array + drop: + items: + description: Capability represent POSIX capabilities type + type: string + type: array + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxOptions: + description: SELinuxOptionsApplyConfiguration represents a... + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + description: SeccompProfileApplyConfiguration represents a... + properties: + localhostProfile: + type: string + type: + description: SeccompProfileType defines the supported seccomp... + type: string + type: object + windowsOptions: + description: WindowsSecurityContextOptionsApplyConfiguration... + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + startupProbe: + description: ProbeApplyConfiguration represents a declarative... + properties: + exec: + description: ExecActionApplyConfiguration represents a... + properties: + command: + items: + type: string + type: array + type: object + failureThreshold: + format: int32 + type: integer + grpc: + description: GRPCActionApplyConfiguration represents a... + properties: + port: + format: int32 + type: integer + service: + type: string + type: object + httpGet: + description: HTTPGetActionApplyConfiguration represents a... + properties: + host: + type: string + httpHeaders: + items: + description: HTTPHeaderApplyConfiguration represents a... + properties: + name: + type: string + value: + type: string + type: object + type: array + path: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + scheme: + description: URIScheme identifies the scheme used for... + type: string + type: object + initialDelaySeconds: + format: int32 + type: integer + periodSeconds: + format: int32 + type: integer + successThreshold: + format: int32 + type: integer + tcpSocket: + description: TCPSocketActionApplyConfiguration represents a... + properties: + host: + type: string + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + timeoutSeconds: + format: int32 + type: integer + type: object + stdin: + type: boolean + stdinOnce: + type: boolean + terminationMessagePath: + type: string + terminationMessagePolicy: + description: TerminationMessagePolicy describes how... + type: string + tty: + type: boolean + volumeDevices: + items: + description: VolumeDeviceApplyConfiguration represents a... + properties: + devicePath: + type: string + name: + type: string + type: object + type: array + volumeMounts: + items: + description: VolumeMountApplyConfiguration represents a... + properties: + mountPath: + type: string + mountPropagation: + description: MountPropagationMode describes mount propagation. + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + description: RecursiveReadOnlyMode describes... + type: string + subPath: + type: string + subPathExpr: + type: string + type: object + type: array + workingDir: + type: string + type: object + type: array + nodeName: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + os: + description: PodOSApplyConfiguration represents a declarative... + properties: + name: + description: OSName is the set of OS'es that can be used in OS. + type: string + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + preemptionPolicy: + description: PreemptionPolicy describes a policy for if/when... + type: string + priority: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + description: PodReadinessGateApplyConfiguration represents a... + properties: + conditionType: + description: PodConditionType is a valid value for... + type: string + type: object + type: array + resourceClaims: + items: + description: PodResourceClaimApplyConfiguration represents a... + properties: + name: + type: string + resourceClaimName: + type: string + resourceClaimTemplateName: + type: string + type: object + type: array + resources: + description: ResourceRequirementsApplyConfiguration represents... + properties: + claims: + items: + description: ResourceClaimApplyConfiguration represents a... + properties: + name: + type: string + request: + type: string + type: object + type: array + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + type: object + restartPolicy: + description: RestartPolicy describes how the container should... + type: string + runtimeClassName: + type: string + schedulerName: + type: string + schedulingGates: + items: + description: PodSchedulingGateApplyConfiguration represents a... + properties: + name: + type: string + type: object + type: array + securityContext: + description: PodSecurityContextApplyConfiguration represents a... + properties: + appArmorProfile: + description: AppArmorProfileApplyConfiguration represents a... + properties: + localhostProfile: + type: string + type: + type: string + type: object + fsGroup: + format: int64 + type: integer + fsGroupChangePolicy: + description: PodFSGroupChangePolicy holds policies that will... + type: string + runAsGroup: + format: int64 + type: integer + runAsNonRoot: + type: boolean + runAsUser: + format: int64 + type: integer + seLinuxChangePolicy: + description: PodSELinuxChangePolicy defines how the... + type: string + seLinuxOptions: + description: SELinuxOptionsApplyConfiguration represents a... + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + description: SeccompProfileApplyConfiguration represents a... + properties: + localhostProfile: + type: string + type: + description: SeccompProfileType defines the supported seccomp... + type: string + type: object + supplementalGroups: + items: + format: int64 + type: integer + type: array + supplementalGroupsPolicy: + description: SupplementalGroupsPolicy defines how supplemental... + type: string + sysctls: + items: + description: SysctlApplyConfiguration represents a declarative... + properties: + name: + type: string + value: + type: string + type: object + type: array + windowsOptions: + description: WindowsSecurityContextOptionsApplyConfiguration... + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + serviceAccount: + type: string + serviceAccountName: + type: string + setHostnameAsFQDN: + type: boolean + shareProcessNamespace: + type: boolean + subdomain: + type: string + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + description: TolerationApplyConfiguration represents a... + properties: + effect: + type: string + key: + type: string + operator: + description: A toleration operator is the set of operators... + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + topologySpreadConstraints: + items: + description: TopologySpreadConstraintApplyConfiguration... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + matchLabelKeys: + items: + type: string + type: array + maxSkew: + format: int32 + type: integer + minDomains: + format: int32 + type: integer + nodeAffinityPolicy: + description: NodeInclusionPolicy defines the type of node... + type: string + nodeTaintsPolicy: + description: NodeInclusionPolicy defines the type of node... + type: string + topologyKey: + type: string + whenUnsatisfiable: + type: string + type: object + type: array + volumes: + items: + description: VolumeApplyConfiguration represents a declarative... + properties: + awsElasticBlockStore: + description: AWSElasticBlockStoreVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + type: object + azureDisk: + description: AzureDiskVolumeSourceApplyConfiguration... + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + type: object + azureFile: + description: AzureFileVolumeSourceApplyConfiguration... + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + type: object + cephfs: + description: CephFSVolumeSourceApplyConfiguration represents a... + properties: + monitors: + items: + type: string + type: array + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + user: + type: string + type: object + cinder: + description: CinderVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + volumeID: + type: string + type: object + configMap: + description: ConfigMapVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + csi: + description: CSIVolumeSourceApplyConfiguration represents a... + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + type: object + downwardAPI: + description: DownwardAPIVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: DownwardAPIVolumeFileApplyConfiguration... + properties: + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + type: object + type: array + type: object + emptyDir: + description: EmptyDirVolumeSourceApplyConfiguration represents... + properties: + medium: + description: StorageMedium defines ways that storage can be... + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: EphemeralVolumeSourceApplyConfiguration... + properties: + volumeClaimTemplate: + description: PersistentVolumeClaimTemplateApplyConfiguration... + properties: + metadata: + description: ObjectMetaApplyConfiguration represents a... + properties: + annotations: + additionalProperties: + type: string + type: object + creationTimestamp: + format: date-time + type: string + deletionGracePeriodSeconds: + format: int64 + type: integer + deletionTimestamp: + format: date-time + type: string + finalizers: + items: + type: string + type: array + generateName: + type: string + generation: + format: int64 + type: integer + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + ownerReferences: + items: + description: OwnerReferenceApplyConfiguration represents a... + properties: + apiVersion: + type: string + blockOwnerDeletion: + type: boolean + controller: + type: boolean + kind: + type: string + name: + type: string + uid: + description: UID is a type that holds unique ID values,... + type: string + type: object + type: array + resourceVersion: + type: string + uid: + description: UID is a type that holds unique ID values,... + type: string + type: object + spec: + description: PersistentVolumeClaimSpecApplyConfiguration... + properties: + accessModes: + items: + type: string + type: array + dataSource: + description: TypedLocalObjectReferenceApplyConfiguration... + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + type: object + dataSourceRef: + description: TypedObjectReferenceApplyConfiguration represents... + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + type: object + resources: + description: VolumeResourceRequirementsApplyConfiguration... + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + type: object + selector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + description: PersistentVolumeMode describes how a volume is... + type: string + volumeName: + type: string + type: object + type: object + type: object + fc: + description: FCVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + wwids: + items: + type: string + type: array + type: object + flexVolume: + description: FlexVolumeSourceApplyConfiguration represents a... + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + type: object + flocker: + description: FlockerVolumeSourceApplyConfiguration represents... + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + description: GCEPersistentDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + type: object + gitRepo: + description: GitRepoVolumeSourceApplyConfiguration represents... + properties: + directory: + type: string + repository: + type: string + revision: + type: string + type: object + glusterfs: + description: GlusterfsVolumeSourceApplyConfiguration... + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + type: object + hostPath: + description: HostPathVolumeSourceApplyConfiguration represents... + properties: + path: + type: string + type: + type: string + type: object + image: + description: ImageVolumeSourceApplyConfiguration represents a... + properties: + pullPolicy: + description: PullPolicy describes a policy for if/when to pull... + type: string + reference: + type: string + type: object + iscsi: + description: ISCSIVolumeSourceApplyConfiguration represents a... + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + targetPortal: + type: string + type: object + name: + type: string + nfs: + description: NFSVolumeSourceApplyConfiguration represents a... + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + type: object + persistentVolumeClaim: + description: PersistentVolumeClaimVolumeSourceApplyConfiguratio... + properties: + claimName: + type: string + readOnly: + type: boolean + type: object + photonPersistentDisk: + description: PhotonPersistentDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + pdID: + type: string + type: object + portworxVolume: + description: PortworxVolumeSourceApplyConfiguration represents... + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + type: object + projected: + description: ProjectedVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + description: VolumeProjectionApplyConfiguration represents a... + properties: + clusterTrustBundle: + description: ClusterTrustBundleProjectionApplyConfiguration... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + type: object + configMap: + description: ConfigMapProjectionApplyConfiguration represents... + properties: + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + downwardAPI: + description: DownwardAPIProjectionApplyConfiguration... + properties: + items: + items: + description: DownwardAPIVolumeFileApplyConfiguration... + properties: + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + type: object + type: array + type: object + podCertificate: + description: PodCertificateProjectionApplyConfiguration... + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + type: object + secret: + description: SecretProjectionApplyConfiguration represents a... + properties: + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + serviceAccountToken: + description: ServiceAccountTokenProjectionApplyConfiguration... + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + type: object + type: object + type: array + type: object + quobyte: + description: QuobyteVolumeSourceApplyConfiguration represents... + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + type: object + rbd: + description: RBDVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + pool: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + user: + type: string + type: object + scaleIO: + description: ScaleIOVolumeSourceApplyConfiguration represents... + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + type: object + secret: + description: SecretVolumeSourceApplyConfiguration represents a... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + optional: + type: boolean + secretName: + type: string + type: object + storageos: + description: StorageOSVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + description: VsphereVirtualDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + type: object + type: object + type: array + type: object + required: + - spec + type: object + primaryServiceTemplate: + description: PrimaryServiceTemplate is a `Service` template... + properties: + metadata: + description: Standard object's metadata. + properties: + annotations: + additionalProperties: + type: string + description: Annotations is a map of string keys and values. + type: object + labels: + additionalProperties: + type: string + description: Labels is a map of string keys and values. + type: object + name: + description: Name is the name of the object. + type: string + type: object + spec: + description: Spec is the ServiceSpec + properties: + allocateLoadBalancerNodePorts: + type: boolean + clusterIP: + type: string + clusterIPs: + items: + type: string + type: array + externalIPs: + items: + type: string + type: array + externalName: + type: string + externalTrafficPolicy: + description: ServiceExternalTrafficPolicy describes how nodes... + type: string + healthCheckNodePort: + format: int32 + type: integer + internalTrafficPolicy: + description: ServiceInternalTrafficPolicy describes how nodes... + type: string + ipFamilies: + items: + description: IPFamily represents the IP Family (IPv4 or IPv6). + type: string + type: array + ipFamilyPolicy: + description: IPFamilyPolicy represents the dual-stack-ness... + type: string + loadBalancerClass: + type: string + loadBalancerIP: + type: string + loadBalancerSourceRanges: + items: + type: string + type: array + ports: + items: + description: ServicePortApplyConfiguration represents a... + properties: + appProtocol: + type: string + name: + type: string + nodePort: + format: int32 + type: integer + port: + format: int32 + type: integer + protocol: + description: Protocol defines network protocols supported for... + type: string + targetPort: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + type: array + publishNotReadyAddresses: + type: boolean + selector: + additionalProperties: + type: string + type: object + sessionAffinity: + description: Session Affinity Type string + type: string + sessionAffinityConfig: + description: SessionAffinityConfigApplyConfiguration... + properties: + clientIP: + description: ClientIPConfigApplyConfiguration represents a... + properties: + timeoutSeconds: + format: int32 + type: integer + type: object + type: object + trafficDistribution: + type: string + type: + description: Service Type string describes ingress methods for... + type: string + type: object + type: object + replicaServiceTemplate: + description: ReplicaServiceTemplate is a `Service` template... + properties: + metadata: + description: Standard object's metadata. + properties: + annotations: + additionalProperties: + type: string + description: Annotations is a map of string keys and values. + type: object + labels: + additionalProperties: + type: string + description: Labels is a map of string keys and values. + type: object + name: + description: Name is the name of the object. + type: string + type: object + spec: + description: Spec is the ServiceSpec + properties: + allocateLoadBalancerNodePorts: + type: boolean + clusterIP: + type: string + clusterIPs: + items: + type: string + type: array + externalIPs: + items: + type: string + type: array + externalName: + type: string + externalTrafficPolicy: + description: ServiceExternalTrafficPolicy describes how nodes... + type: string + healthCheckNodePort: + format: int32 + type: integer + internalTrafficPolicy: + description: ServiceInternalTrafficPolicy describes how nodes... + type: string + ipFamilies: + items: + description: IPFamily represents the IP Family (IPv4 or IPv6). + type: string + type: array + ipFamilyPolicy: + description: IPFamilyPolicy represents the dual-stack-ness... + type: string + loadBalancerClass: + type: string + loadBalancerIP: + type: string + loadBalancerSourceRanges: + items: + type: string + type: array + ports: + items: + description: ServicePortApplyConfiguration represents a... + properties: + appProtocol: + type: string + name: + type: string + nodePort: + format: int32 + type: integer + port: + format: int32 + type: integer + protocol: + description: Protocol defines network protocols supported for... + type: string + targetPort: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + type: array + publishNotReadyAddresses: + type: boolean + selector: + additionalProperties: + type: string + type: object + sessionAffinity: + description: Session Affinity Type string + type: string + sessionAffinityConfig: + description: SessionAffinityConfigApplyConfiguration... + properties: + clientIP: + description: ClientIPConfigApplyConfiguration represents a... + properties: + timeoutSeconds: + format: int32 + type: integer + type: object + type: object + trafficDistribution: + type: string + type: + description: Service Type string describes ingress methods for... + type: string + type: object + type: object + replicas: + default: 1 + description: Replicas is the number of instances. + format: int32 + type: integer + replicationSourceSecretName: + description: ReplicationSourceSecretName is a `Secret` name... + nullable: true + type: string + restore: + description: Restore is the specification to perform... + properties: + jobConfig: + description: Specifies parameters for restore Pod. + properties: + affinity: + description: If specified, the pod's scheduling constraints. + properties: + nodeAffinity: + description: NodeAffinityApplyConfiguration represents a... + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + description: PreferredSchedulingTermApplyConfiguration... + properties: + preference: + description: NodeSelectorTermApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: NodeSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A node selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchFields: + items: + description: NodeSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A node selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + type: object + weight: + format: int32 + type: integer + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: NodeSelectorApplyConfiguration represents a... + properties: + nodeSelectorTerms: + items: + description: NodeSelectorTermApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: NodeSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A node selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchFields: + items: + description: NodeSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A node selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + type: object + type: array + type: object + type: object + podAffinity: + description: PodAffinityApplyConfiguration represents a... + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + description: WeightedPodAffinityTermApplyConfiguration... + properties: + podAffinityTerm: + description: PodAffinityTermApplyConfiguration represents a... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + matchLabelKeys: + items: + type: string + type: array + mismatchLabelKeys: + items: + type: string + type: array + namespaceSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + namespaces: + items: + type: string + type: array + topologyKey: + type: string + type: object + weight: + format: int32 + type: integer + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + description: PodAffinityTermApplyConfiguration represents a... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + matchLabelKeys: + items: + type: string + type: array + mismatchLabelKeys: + items: + type: string + type: array + namespaceSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + namespaces: + items: + type: string + type: array + topologyKey: + type: string + type: object + type: array + type: object + podAntiAffinity: + description: PodAntiAffinityApplyConfiguration represents a... + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + description: WeightedPodAffinityTermApplyConfiguration... + properties: + podAffinityTerm: + description: PodAffinityTermApplyConfiguration represents a... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + matchLabelKeys: + items: + type: string + type: array + mismatchLabelKeys: + items: + type: string + type: array + namespaceSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + namespaces: + items: + type: string + type: array + topologyKey: + type: string + type: object + weight: + format: int32 + type: integer + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + items: + description: PodAffinityTermApplyConfiguration represents a... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + matchLabelKeys: + items: + type: string + type: array + mismatchLabelKeys: + items: + type: string + type: array + namespaceSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + namespaces: + items: + type: string + type: array + topologyKey: + type: string + type: object + type: array + type: object + type: object + bucketConfig: + description: Specifies how to access an object storage bucket. + properties: + backendType: + default: s3 + description: BackendType is an identifier for the object... + enum: + - s3 + - gcs + type: string + bucketName: + description: The name of the bucket + minLength: 1 + type: string + caCert: + description: Path to SSL CA certificate file used in addition... + type: string + endpointURL: + description: The API endpoint URL. + pattern: ^https?://.* + type: string + region: + description: The region of the bucket. + type: string + usePathStyle: + description: Allows you to enable the client to use path-style... + type: boolean + required: + - bucketName + type: object + cpu: + anyOf: + - type: integer + - type: string + default: 4 + description: CPU is the amount of CPU requested for the Pod. + nullable: true + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + env: + description: List of environment variables to set in the... + items: + description: EnvVarApplyConfiguration is the type defined to... + properties: + name: + type: string + value: + type: string + valueFrom: + description: EnvVarSourceApplyConfiguration represents a... + properties: + configMapKeyRef: + description: ConfigMapKeySelectorApplyConfiguration represents... + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + fileKeyRef: + description: FileKeySelectorApplyConfiguration represents a... + properties: + key: + type: string + optional: + type: boolean + path: + type: string + volumeName: + type: string + type: object + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + secretKeyRef: + description: SecretKeySelectorApplyConfiguration represents a... + properties: + key: + type: string + name: + type: string + optional: + type: boolean + type: object + type: object + type: object + type: array + envFrom: + description: List of sources to populate environment variables... + items: + description: EnvFromSourceApplyConfiguration is the type... + properties: + configMapRef: + description: ConfigMapEnvSourceApplyConfiguration represents a... + properties: + name: + type: string + optional: + type: boolean + type: object + prefix: + type: string + secretRef: + description: SecretEnvSourceApplyConfiguration represents a... + properties: + name: + type: string + optional: + type: boolean + type: object + type: object + type: array + maxCpu: + anyOf: + - type: integer + - type: string + description: MaxCPU is the amount of maximum CPU for the Pod. + nullable: true + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + maxMemory: + anyOf: + - type: integer + - type: string + description: MaxMemory is the amount of maximum memory for the... + nullable: true + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + memory: + anyOf: + - type: integer + - type: string + default: 4Gi + description: Memory is the amount of memory requested for the... + nullable: true + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + serviceAccountName: + description: ServiceAccountName specifies the ServiceAccount... + minLength: 1 + type: string + threads: + default: 4 + description: Threads is the number of threads used for backup... + minimum: 1 + type: integer + volumeMounts: + description: VolumeMounts describes a list of volume mounts... + items: + description: VolumeMountApplyConfiguration is the type defined... + properties: + mountPath: + type: string + mountPropagation: + description: MountPropagationMode describes mount propagation. + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + description: RecursiveReadOnlyMode describes... + type: string + subPath: + type: string + subPathExpr: + type: string + type: object + type: array + volumes: + description: Volumes defines the list of volumes that can be... + items: + description: VolumeApplyConfiguration is the type defined to... + properties: + awsElasticBlockStore: + description: AWSElasticBlockStoreVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + type: object + azureDisk: + description: AzureDiskVolumeSourceApplyConfiguration... + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + type: object + azureFile: + description: AzureFileVolumeSourceApplyConfiguration... + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + type: object + cephfs: + description: CephFSVolumeSourceApplyConfiguration represents a... + properties: + monitors: + items: + type: string + type: array + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + user: + type: string + type: object + cinder: + description: CinderVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + volumeID: + type: string + type: object + configMap: + description: ConfigMapVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + csi: + description: CSIVolumeSourceApplyConfiguration represents a... + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + type: object + downwardAPI: + description: DownwardAPIVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: DownwardAPIVolumeFileApplyConfiguration... + properties: + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + type: object + type: array + type: object + emptyDir: + description: EmptyDirVolumeSourceApplyConfiguration represents... + properties: + medium: + description: StorageMedium defines ways that storage can be... + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: EphemeralVolumeSourceApplyConfiguration... + properties: + volumeClaimTemplate: + description: PersistentVolumeClaimTemplateApplyConfiguration... + properties: + metadata: + description: ObjectMetaApplyConfiguration represents a... + properties: + annotations: + additionalProperties: + type: string + type: object + creationTimestamp: + format: date-time + type: string + deletionGracePeriodSeconds: + format: int64 + type: integer + deletionTimestamp: + format: date-time + type: string + finalizers: + items: + type: string + type: array + generateName: + type: string + generation: + format: int64 + type: integer + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + ownerReferences: + items: + description: OwnerReferenceApplyConfiguration represents a... + properties: + apiVersion: + type: string + blockOwnerDeletion: + type: boolean + controller: + type: boolean + kind: + type: string + name: + type: string + uid: + description: UID is a type that holds unique ID values,... + type: string + type: object + type: array + resourceVersion: + type: string + uid: + description: UID is a type that holds unique ID values,... + type: string + type: object + spec: + description: PersistentVolumeClaimSpecApplyConfiguration... + properties: + accessModes: + items: + type: string + type: array + dataSource: + description: TypedLocalObjectReferenceApplyConfiguration... + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + type: object + dataSourceRef: + description: TypedObjectReferenceApplyConfiguration represents... + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + type: object + resources: + description: VolumeResourceRequirementsApplyConfiguration... + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + type: object + selector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + description: PersistentVolumeMode describes how a volume is... + type: string + volumeName: + type: string + type: object + type: object + type: object + fc: + description: FCVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + wwids: + items: + type: string + type: array + type: object + flexVolume: + description: FlexVolumeSourceApplyConfiguration represents a... + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + type: object + flocker: + description: FlockerVolumeSourceApplyConfiguration represents... + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + description: GCEPersistentDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + type: object + gitRepo: + description: GitRepoVolumeSourceApplyConfiguration represents... + properties: + directory: + type: string + repository: + type: string + revision: + type: string + type: object + glusterfs: + description: GlusterfsVolumeSourceApplyConfiguration... + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + type: object + hostPath: + description: HostPathVolumeSourceApplyConfiguration represents... + properties: + path: + type: string + type: + type: string + type: object + image: + description: ImageVolumeSourceApplyConfiguration represents a... + properties: + pullPolicy: + description: PullPolicy describes a policy for if/when to pull... + type: string + reference: + type: string + type: object + iscsi: + description: ISCSIVolumeSourceApplyConfiguration represents a... + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + targetPortal: + type: string + type: object + name: + type: string + nfs: + description: NFSVolumeSourceApplyConfiguration represents a... + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + type: object + persistentVolumeClaim: + description: PersistentVolumeClaimVolumeSourceApplyConfiguratio... + properties: + claimName: + type: string + readOnly: + type: boolean + type: object + photonPersistentDisk: + description: PhotonPersistentDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + pdID: + type: string + type: object + portworxVolume: + description: PortworxVolumeSourceApplyConfiguration represents... + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + type: object + projected: + description: ProjectedVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + description: VolumeProjectionApplyConfiguration represents a... + properties: + clusterTrustBundle: + description: ClusterTrustBundleProjectionApplyConfiguration... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + type: object + configMap: + description: ConfigMapProjectionApplyConfiguration represents... + properties: + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + downwardAPI: + description: DownwardAPIProjectionApplyConfiguration... + properties: + items: + items: + description: DownwardAPIVolumeFileApplyConfiguration... + properties: + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + type: object + type: array + type: object + podCertificate: + description: PodCertificateProjectionApplyConfiguration... + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + type: object + secret: + description: SecretProjectionApplyConfiguration represents a... + properties: + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + serviceAccountToken: + description: ServiceAccountTokenProjectionApplyConfiguration... + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + type: object + type: object + type: array + type: object + quobyte: + description: QuobyteVolumeSourceApplyConfiguration represents... + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + type: object + rbd: + description: RBDVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + pool: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + user: + type: string + type: object + scaleIO: + description: ScaleIOVolumeSourceApplyConfiguration represents... + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + type: object + secret: + description: SecretVolumeSourceApplyConfiguration represents a... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + optional: + type: boolean + secretName: + type: string + type: object + storageos: + description: StorageOSVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + description: VsphereVirtualDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + type: object + type: object + type: array + workVolume: + description: WorkVolume is the volume source for the working... + properties: + awsElasticBlockStore: + description: AWSElasticBlockStoreVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + partition: + format: int32 + type: integer + readOnly: + type: boolean + volumeID: + type: string + type: object + azureDisk: + description: AzureDiskVolumeSourceApplyConfiguration... + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + type: object + azureFile: + description: AzureFileVolumeSourceApplyConfiguration... + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + type: object + cephfs: + description: CephFSVolumeSourceApplyConfiguration represents a... + properties: + monitors: + items: + type: string + type: array + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + user: + type: string + type: object + cinder: + description: CinderVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + volumeID: + type: string + type: object + configMap: + description: ConfigMapVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + csi: + description: CSIVolumeSourceApplyConfiguration represents a... + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + type: object + downwardAPI: + description: DownwardAPIVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: DownwardAPIVolumeFileApplyConfiguration... + properties: + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + type: object + type: array + type: object + emptyDir: + description: EmptyDirVolumeSourceApplyConfiguration represents... + properties: + medium: + description: StorageMedium defines ways that storage can be... + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: EphemeralVolumeSourceApplyConfiguration... + properties: + volumeClaimTemplate: + description: PersistentVolumeClaimTemplateApplyConfiguration... + properties: + metadata: + description: ObjectMetaApplyConfiguration represents a... + properties: + annotations: + additionalProperties: + type: string + type: object + creationTimestamp: + format: date-time + type: string + deletionGracePeriodSeconds: + format: int64 + type: integer + deletionTimestamp: + format: date-time + type: string + finalizers: + items: + type: string + type: array + generateName: + type: string + generation: + format: int64 + type: integer + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + ownerReferences: + items: + description: OwnerReferenceApplyConfiguration represents a... + properties: + apiVersion: + type: string + blockOwnerDeletion: + type: boolean + controller: + type: boolean + kind: + type: string + name: + type: string + uid: + description: UID is a type that holds unique ID values,... + type: string + type: object + type: array + resourceVersion: + type: string + uid: + description: UID is a type that holds unique ID values,... + type: string + type: object + spec: + description: PersistentVolumeClaimSpecApplyConfiguration... + properties: + accessModes: + items: + type: string + type: array + dataSource: + description: TypedLocalObjectReferenceApplyConfiguration... + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + type: object + dataSourceRef: + description: TypedObjectReferenceApplyConfiguration represents... + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + type: object + resources: + description: VolumeResourceRequirementsApplyConfiguration... + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + type: object + selector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + description: PersistentVolumeMode describes how a volume is... + type: string + volumeName: + type: string + type: object + type: object + type: object + fc: + description: FCVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + lun: + format: int32 + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + wwids: + items: + type: string + type: array + type: object + flexVolume: + description: FlexVolumeSourceApplyConfiguration represents a... + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + type: object + flocker: + description: FlockerVolumeSourceApplyConfiguration represents... + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + description: GCEPersistentDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + partition: + format: int32 + type: integer + pdName: + type: string + readOnly: + type: boolean + type: object + gitRepo: + description: GitRepoVolumeSourceApplyConfiguration represents... + properties: + directory: + type: string + repository: + type: string + revision: + type: string + type: object + glusterfs: + description: GlusterfsVolumeSourceApplyConfiguration... + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + type: object + hostPath: + description: HostPathVolumeSourceApplyConfiguration represents... + properties: + path: + type: string + type: + type: string + type: object + image: + description: ImageVolumeSourceApplyConfiguration represents a... + properties: + pullPolicy: + description: PullPolicy describes a policy for if/when to pull... + type: string + reference: + type: string + type: object + iscsi: + description: ISCSIVolumeSourceApplyConfiguration represents a... + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + format: int32 + type: integer + portals: + items: + type: string + type: array + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + targetPortal: + type: string + type: object + nfs: + description: NFSVolumeSourceApplyConfiguration represents a... + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + type: object + persistentVolumeClaim: + description: PersistentVolumeClaimVolumeSourceApplyConfiguratio... + properties: + claimName: + type: string + readOnly: + type: boolean + type: object + photonPersistentDisk: + description: PhotonPersistentDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + pdID: + type: string + type: object + portworxVolume: + description: PortworxVolumeSourceApplyConfiguration represents... + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + type: object + projected: + description: ProjectedVolumeSourceApplyConfiguration... + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + description: VolumeProjectionApplyConfiguration represents a... + properties: + clusterTrustBundle: + description: ClusterTrustBundleProjectionApplyConfiguration... + properties: + labelSelector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + type: object + configMap: + description: ConfigMapProjectionApplyConfiguration represents... + properties: + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + downwardAPI: + description: DownwardAPIProjectionApplyConfiguration... + properties: + items: + items: + description: DownwardAPIVolumeFileApplyConfiguration... + properties: + fieldRef: + description: ObjectFieldSelectorApplyConfiguration represents... + properties: + apiVersion: + type: string + fieldPath: + type: string + type: object + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + description: ResourceFieldSelectorApplyConfiguration... + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + type: object + type: object + type: array + type: object + podCertificate: + description: PodCertificateProjectionApplyConfiguration... + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + format: int32 + type: integer + signerName: + type: string + type: object + secret: + description: SecretProjectionApplyConfiguration represents a... + properties: + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + name: + type: string + optional: + type: boolean + type: object + serviceAccountToken: + description: ServiceAccountTokenProjectionApplyConfiguration... + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + type: object + type: object + type: array + type: object + quobyte: + description: QuobyteVolumeSourceApplyConfiguration represents... + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + type: object + rbd: + description: RBDVolumeSourceApplyConfiguration represents a... + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + pool: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + user: + type: string + type: object + scaleIO: + description: ScaleIOVolumeSourceApplyConfiguration represents... + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + type: object + secret: + description: SecretVolumeSourceApplyConfiguration represents a... + properties: + defaultMode: + format: int32 + type: integer + items: + items: + description: KeyToPathApplyConfiguration represents a... + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + type: object + type: array + optional: + type: boolean + secretName: + type: string + type: object + storageos: + description: StorageOSVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + description: LocalObjectReferenceApplyConfiguration represents... + properties: + name: + type: string + type: object + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + description: VsphereVirtualDiskVolumeSourceApplyConfiguration... + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + type: object + type: object + required: + - bucketConfig + - serviceAccountName + - workVolume + type: object + restorePoint: + description: RestorePoint is the target date and time to... + format: date-time + type: string + schema: + description: Schema is the name of the schema to restore. + type: string + sourceName: + description: SourceName is the name of the source... + minLength: 1 + type: string + sourceNamespace: + description: SourceNamespace is the namespace of the source... + minLength: 1 + type: string + users: + description: Users is the name of the comma separated users to... + type: string + required: + - jobConfig + - restorePoint + - sourceName + - sourceNamespace + type: object + serverIDBase: + description: ServerIDBase, if set, will become the base number... + format: int32 + type: integer + slowQueryLogConfigTmpl: + description: SlowQueryLogConfigTmpl is the template for slow... + type: string + startupWaitSeconds: + default: 3600 + description: StartupWaitSeconds is the maximum duration to... + format: int32 + minimum: 0 + type: integer + volumeClaimTemplates: + description: VolumeClaimTemplates is a list of... + items: + description: PersistentVolumeClaim is a user's request for and... + properties: + metadata: + description: Standard object's metadata. + properties: + annotations: + additionalProperties: + type: string + description: Annotations is a map of string keys and values. + type: object + labels: + additionalProperties: + type: string + description: Labels is a map of string keys and values. + type: object + name: + description: Name is the name of the object. + type: string + type: object + spec: + description: Spec defines the desired characteristics of a... + properties: + accessModes: + items: + type: string + type: array + dataSource: + description: TypedLocalObjectReferenceApplyConfiguration... + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + type: object + dataSourceRef: + description: TypedObjectReferenceApplyConfiguration represents... + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + type: object + resources: + description: VolumeResourceRequirementsApplyConfiguration... + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: ResourceList is a set of (resource name,... + type: object + type: object + selector: + description: LabelSelectorApplyConfiguration represents a... + properties: + matchExpressions: + items: + description: LabelSelectorRequirementApplyConfiguration... + properties: + key: + type: string + operator: + description: A label selector operator is the set of operators... + type: string + values: + items: + type: string + type: array + type: object + type: array + matchLabels: + additionalProperties: + type: string + type: object + type: object + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + description: PersistentVolumeMode describes how a volume is... + type: string + volumeName: + type: string + type: object + required: + - metadata + - spec + type: object + minItems: 1 + type: array + required: + - podTemplate + - volumeClaimTemplates + type: object + status: + description: MySQLClusterStatus defines the observed state of... + properties: + backup: + description: Backup is the status of the last successful... + properties: + binlogFilename: + description: BinlogFilename is the binlog filename that the... + type: string + binlogSize: + description: BinlogSize is the size in bytes of a tarball of... + format: int64 + type: integer + dumpSize: + description: DumpSize is the size in bytes of a full dump of... + format: int64 + type: integer + elapsed: + description: Elapsed is the time spent on the backup. + type: string + gtidSet: + description: GTIDSet is the GTID set of the full dump of... + type: string + sourceIndex: + description: SourceIndex is the ordinal of the backup source... + type: integer + sourceUUID: + description: SourceUUID is the `server_uuid` of the backup... + type: string + time: + description: The time of the backup. + format: date-time + nullable: true + type: string + uuidSet: + additionalProperties: + type: string + description: UUIDSet is the `server_uuid` set of all candidate... + type: object + warnings: + description: Warnings are list of warnings from the last... + items: + type: string + nullable: true + type: array + workDirUsage: + description: WorkDirUsage is the max usage in bytes of the... + format: int64 + type: integer + required: + - binlogFilename + - binlogSize + - dumpSize + - elapsed + - gtidSet + - sourceIndex + - sourceUUID + - time + - warnings + - workDirUsage + type: object + cloned: + description: Cloned indicates if the initial cloning from an... + type: boolean + conditions: + description: Conditions is an array of conditions. + items: + description: Condition contains details for one aspect of the... + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition... + format: date-time + type: string + message: + description: message is a human readable message indicating... + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier... + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False,... + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example. + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + currentPrimaryIndex: + description: CurrentPrimaryIndex is the index of the current... + type: integer + errantReplicaList: + description: ErrantReplicaList is the list of indices of... + items: + type: integer + type: array + errantReplicas: + description: ErrantReplicas is the number of instances that... + type: integer + reconcileInfo: + description: ReconcileInfo represents version information for... + properties: + generation: + description: Generation is the `metadata. + format: int64 + type: integer + reconcileVersion: + description: ReconcileVersion is the version of the operator... + type: integer + type: object + restoredTime: + description: RestoredTime is the time when the cluster data is... + format: date-time + type: string + syncedReplicas: + description: SyncedReplicas is the number of synced instances... + type: integer + required: + - currentPrimaryIndex + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/vendored/redis-operator/README.md b/pkg/vendored/redis-operator/README.md new file mode 100644 index 00000000..0390fce7 --- /dev/null +++ b/pkg/vendored/redis-operator/README.md @@ -0,0 +1,109 @@ +# Redis Operator API Vendored Code + +This directory contains vendored API types from the [OT-CONTAINER-KIT/redis-operator](https://github.com/OT-CONTAINER-KIT/redis-operator) project. + +## Source + +- **Repository**: https://github.com/OT-CONTAINER-KIT/redis-operator +- **Version**: v0.22.1 +- **Date Vendored**: 2025-01-28 + +## Reason for Vendoring + +The redis-operator v0.22.1 has a compilation error in its webhook code that is incompatible with controller-runtime versions used by this project: + +``` +redissentinel_webhook.go:46:15: undefined: webhook.Validator +``` + +The webhook code references `webhook.Validator` which doesn't exist in any controller-runtime version - it should be `admission.Validator` or `admission.CustomValidator`. + +## Changes Made + +### Removed Code Generation Annotations + +All code generation annotations have been removed from vendored code: +- `+kubebuilder:*` annotations (e.g., `+kubebuilder:validation:*`, `+kubebuilder:default:*`, `+kubebuilder:object:*`) +- `+k8s:*` annotations (e.g., `+k8s:deepcopy-gen:*`, `+k8s:openapi-gen=true`) + +These annotations are only needed when running code generators like controller-gen or deepcopy-gen, which we explicitly do not do for vendored APIs. The Makefile's `generate` target only processes our own `./api/v1` and `./api/v2` types. + +Files affected: +- `common/v1beta2/common_types.go` +- `common/v1beta2/groupversion_info.go` +- `redis/v1beta2/redis_types.go` +- `redis/v1beta2/groupversion_info.go` +- `redisreplication/v1beta2/redisreplication_types.go` +- `redisreplication/v1beta2/groupversion_info.go` +- `redissentinel/v1beta2/redissentinel_types.go` +- `redissentinel/v1beta2/redissentinel_webhook.go` +- `redissentinel/v1beta2/groupversion_info.go` + +### api/redissentinel/v1beta2/redissentinel_webhook.go +- **Line 26-28**: Commented out unused webhook import +- **Line 46-47**: Commented out the problematic `var _ webhook.Validator = &RedisSentinel{}` interface check +- Added comments explaining the patches + +### Import Path Updates +All vendored files have been updated to reference the vendored `common/v1beta2` package instead of the upstream package: +- `redis/v1beta2/redis_types.go` +- `redissentinel/v1beta2/redissentinel_types.go` +- `redisreplication/v1beta2/redisreplication_types.go` +- Generated deepcopy files + +## What Was Vendored + +We vendored the API type definitions needed for our operator: + +### CRDs + +**NOTE: CRD files must be downloaded separately and are not currently vendored.** + +To download the CRDs for integration testing: + +1. Create the `crds/` directory: + ```bash + mkdir -p pkg/vendored/redis-operator/crds + ``` + +2. Download the Redis Operator CRDs from the upstream repository at v0.22.1: + ```bash + # Redis (standalone) + curl -L https://raw.githubusercontent.com/OT-CONTAINER-KIT/redis-operator/v0.22.1/config/crd/bases/redis.redis.opstreelabs.in_redis.yaml \ + -o pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redis.yaml + + # RedisReplication + curl -L https://raw.githubusercontent.com/OT-CONTAINER-KIT/redis-operator/v0.22.1/config/crd/bases/redis.redis.opstreelabs.in_redisreplications.yaml \ + -o pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redisreplications.yaml + + # RedisSentinel + curl -L https://raw.githubusercontent.com/OT-CONTAINER-KIT/redis-operator/v0.22.1/config/crd/bases/redis.redis.opstreelabs.in_redissentinels.yaml \ + -o pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redissentinels.yaml + + # RedisCluster + curl -L https://raw.githubusercontent.com/OT-CONTAINER-KIT/redis-operator/v0.22.1/config/crd/bases/redis.redis.opstreelabs.in_redisclusters.yaml \ + -o pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redisclusters.yaml + ``` + +3. **When updating to a new version**: Update the version tag (v0.22.1) in the URLs above to match the new vendored version. + +**Purpose**: Integration testing with real Kubernetes API server (envtest) + +### API Types + +- `common/v1beta2/` - Common types and configurations +- `redis/v1beta2/` - Redis CRD types (standalone Redis) +- `redisreplication/v1beta2/` - RedisReplication CRD types (HA replication) +- `redissentinel/v1beta2/` - RedisSentinel CRD types (HA monitoring) + +### Removed Content +- All test files (`*_test.go`) +- Internal operator logic and controllers + +## License + +The vendored code maintains its original Apache 2.0 license from the redis-operator project. + +## Removal + +This vendored copy should be removed once redis-operator releases a fixed version (likely v0.22.2 or later) that is compatible with controller-runtime. diff --git a/pkg/vendored/redis-operator/common/v1beta2/README.md b/pkg/vendored/redis-operator/common/v1beta2/README.md new file mode 100644 index 00000000..f36cde13 --- /dev/null +++ b/pkg/vendored/redis-operator/common/v1beta2/README.md @@ -0,0 +1,19 @@ +# Common API Types (v1beta2) + +## Source +- **Package**: `github.com/OT-CONTAINER-KIT/redis-operator/api/common/v1beta2` +- **Version**: v0.22.1 +- **Commit**: Corresponds to v0.22.1 release tag +- **Cloned From**: `/Users/j7m4/go/pkg/mod/github.com/!o!t-!c!o!n!t!a!i!n!e!r-!k!i!t/redis-operator@v0.22.1/api/common/v1beta2/` + +## Contents +Common types and configuration structures shared across Redis operator CRDs: +- Storage configurations +- Kubernetes configurations +- Redis configurations +- TLS and security settings +- Pod disruption budgets +- Sidecars and init containers + +## Modifications +None - files copied as-is from upstream. diff --git a/pkg/vendored/redis-operator/common/v1beta2/common_types.go b/pkg/vendored/redis-operator/common/v1beta2/common_types.go new file mode 100644 index 00000000..592447fa --- /dev/null +++ b/pkg/vendored/redis-operator/common/v1beta2/common_types.go @@ -0,0 +1,245 @@ +package v1beta2 + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" +) + +// KubernetesConfig will be the JSON struct for Basic Redis Config +type KubernetesConfig struct { + Image string `json:"image"` + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` + ExistingPasswordSecret *ExistingPasswordSecret `json:"redisSecret,omitempty"` + ImagePullSecrets *[]corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + UpdateStrategy appsv1.StatefulSetUpdateStrategy `json:"updateStrategy,omitempty"` + PersistentVolumeClaimRetentionPolicy *appsv1.StatefulSetPersistentVolumeClaimRetentionPolicy `json:"persistentVolumeClaimRetentionPolicy,omitempty"` + Service *ServiceConfig `json:"service,omitempty"` + IgnoreAnnotations []string `json:"ignoreAnnotations,omitempty"` + MinReadySeconds *int32 `json:"minReadySeconds,omitempty"` +} + +func (in *KubernetesConfig) GetServiceType() string { + if in.Service == nil { + return "ClusterIP" + } + return in.Service.ServiceType +} + +func (in *KubernetesConfig) GetServiceAnnotations() map[string]string { + if in.Service == nil { + return nil + } + return in.Service.ServiceAnnotations +} + +func (in *KubernetesConfig) GetHeadlessServiceAnnotations() map[string]string { + if in.Service == nil { + return nil + } + if in.Service.Headless == nil { + return nil + } + return in.Service.Headless.AdditionalAnnotations +} + +// ShouldCreateAdditionalService returns whether additional service should be created +func (in *KubernetesConfig) ShouldCreateAdditionalService() bool { + if in.Service == nil { + return true + } + if in.Service.Additional == nil { + return true + } + if in.Service.Additional.Enabled == nil { + return true + } + return *in.Service.Additional.Enabled +} + +// ShouldIncludeBusPort returns whether bus port should be included in the service +func (in *KubernetesConfig) ShouldIncludeBusPort() bool { + if in.Service == nil { + return false + } + if in.Service.IncludeBusPort == nil { + return false + } + return *in.Service.IncludeBusPort +} + +// ShouldIncludeBusPortForHeadless returns whether bus port should be included in the headless service +func (in *KubernetesConfig) ShouldIncludeBusPortForHeadless() bool { + if in.Service == nil { + return false + } + if in.Service.Headless == nil { + return false + } + if in.Service.Headless.IncludeBusPort == nil { + return false + } + return *in.Service.Headless.IncludeBusPort +} + +// ShouldIncludeBusPortForAdditional returns whether bus port should be included in the additional service +func (in *KubernetesConfig) ShouldIncludeBusPortForAdditional() bool { + if in.Service == nil { + return false + } + if in.Service.Additional == nil { + return false + } + if in.Service.Additional.IncludeBusPort == nil { + return false + } + return *in.Service.Additional.IncludeBusPort +} + +// ServiceConfig define the type of service to be created and its annotations +type ServiceConfig struct { + ServiceType string `json:"serviceType,omitempty"` + ServiceAnnotations map[string]string `json:"annotations,omitempty"` + // IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + // This field is only used for Redis cluster mode. + IncludeBusPort *bool `json:"includeBusPort,omitempty"` + // Headless config for which suffix is -headless service + Headless *Service `json:"headless,omitempty"` + // Additional config for which suffix is -additional service + Additional *Service `json:"additional,omitempty"` +} + +// Service is the struct to define the service type and its annotations +type Service struct { + Type string `json:"type,omitempty"` + AdditionalAnnotations map[string]string `json:"additionalAnnotations,omitempty"` + // IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + // This field is only used for Redis cluster mode. + IncludeBusPort *bool `json:"includeBusPort,omitempty"` + Enabled *bool `json:"enabled,omitempty"` +} + +// ExistingPasswordSecret is the struct to access the existing secret +type ExistingPasswordSecret struct { + Name *string `json:"name,omitempty"` + Key *string `json:"key,omitempty"` +} + +// RedisExporter interface will have the information for redis exporter related stuff +type RedisExporter struct { + Enabled bool `json:"enabled,omitempty"` + Port *int `json:"port,omitempty"` + Image string `json:"image"` + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + EnvVars *[]corev1.EnvVar `json:"env,omitempty"` + SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"` +} + +// RedisConfig defines the external configuration of Redis +type RedisConfig struct { + // MaxMemoryPercentOfLimit is the percentage of redis container memory limit to be used as maxmemory. + MaxMemoryPercentOfLimit *int `json:"maxMemoryPercentOfLimit,omitempty"` + DynamicConfig []string `json:"dynamicConfig,omitempty"` + AdditionalRedisConfig *string `json:"additionalRedisConfig,omitempty"` +} + +// Storage is the inteface to add pvc and pv support in redis +type Storage struct { + KeepAfterDelete bool `json:"keepAfterDelete,omitempty"` + VolumeClaimTemplate corev1.PersistentVolumeClaim `json:"volumeClaimTemplate,omitempty"` + VolumeMount AdditionalVolume `json:"volumeMount,omitempty"` +} + +// Additional Volume is provided by user that is mounted on the pods +type AdditionalVolume struct { + Volume []corev1.Volume `json:"volume,omitempty"` + MountPath []corev1.VolumeMount `json:"mountPath,omitempty"` +} + +// TLS Configuration for redis instances +type TLSConfig struct { + CaKeyFile string `json:"ca,omitempty"` + CertKeyFile string `json:"cert,omitempty"` + KeyFile string `json:"key,omitempty"` + // Reference to secret which contains the certificates + Secret corev1.SecretVolumeSource `json:"secret"` +} + +// Sidecar for each Redis pods +type Sidecar struct { + Name string `json:"name"` + Image string `json:"image"` + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` + EnvVars *[]corev1.EnvVar `json:"env,omitempty"` + Volumes *[]corev1.VolumeMount `json:"mountPath,omitempty"` + Command []string `json:"command,omitempty" protobuf:"bytes,3,rep,name=command"` + Ports *[]corev1.ContainerPort `json:"ports,omitempty" patchStrategy:"merge" patchMergeKey:"containerPort" protobuf:"bytes,6,rep,name=ports"` + SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"` +} + +// RedisLeader interface will have the redis leader configuration +type RedisLeader struct { + // Replicas overrides clusterSize for leader nodes count. If not set, uses clusterSize value + Replicas *int32 `json:"replicas,omitempty"` + RedisConfig *RedisConfig `json:"redisConfig,omitempty"` + Affinity *corev1.Affinity `json:"affinity,omitempty"` + PodDisruptionBudget *RedisPodDisruptionBudget `json:"pdb,omitempty"` + ReadinessProbe *corev1.Probe `json:"readinessProbe,omitempty" protobuf:"bytes,11,opt,name=readinessProbe"` + LivenessProbe *corev1.Probe `json:"livenessProbe,omitempty" protobuf:"bytes,12,opt,name=livenessProbe"` + Tolerations *[]corev1.Toleration `json:"tolerations,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` +} + +// RedisFollower interface will have the redis follower configuration +type RedisFollower struct { + // Replicas overrides clusterSize for follower nodes count. If not set, uses clusterSize value + Replicas *int32 `json:"replicas,omitempty"` + RedisConfig *RedisConfig `json:"redisConfig,omitempty"` + Affinity *corev1.Affinity `json:"affinity,omitempty"` + PodDisruptionBudget *RedisPodDisruptionBudget `json:"pdb,omitempty"` + ReadinessProbe *corev1.Probe `json:"readinessProbe,omitempty" protobuf:"bytes,11,opt,name=readinessProbe"` + LivenessProbe *corev1.Probe `json:"livenessProbe,omitempty" protobuf:"bytes,12,opt,name=livenessProbe"` + Tolerations *[]corev1.Toleration `json:"tolerations,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` +} + +// RedisPodDisruptionBudget configure a PodDisruptionBudget on the resource (leader/follower) +type RedisPodDisruptionBudget struct { + Enabled bool `json:"enabled,omitempty"` + MinAvailable *int32 `json:"minAvailable,omitempty"` + MaxUnavailable *int32 `json:"maxUnavailable,omitempty"` +} + +type RedisSentinelConfig struct { + AdditionalSentinelConfig *string `json:"additionalSentinelConfig,omitempty"` + RedisReplicationName string `json:"redisReplicationName"` + RedisReplicationPassword *corev1.EnvVarSource `json:"redisReplicationPassword,omitempty"` + MasterGroupName string `json:"masterGroupName,omitempty"` + RedisPort string `json:"redisPort,omitempty"` + Quorum string `json:"quorum,omitempty"` + ParallelSyncs string `json:"parallelSyncs,omitempty"` + FailoverTimeout string `json:"failoverTimeout,omitempty"` + DownAfterMilliseconds string `json:"downAfterMilliseconds,omitempty"` + ResolveHostnames string `json:"resolveHostnames,omitempty"` + AnnounceHostnames string `json:"announceHostnames,omitempty"` +} + +// InitContainer for each Redis pods +type InitContainer struct { + Enabled *bool `json:"enabled,omitempty"` + Image string `json:"image"` + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` + EnvVars *[]corev1.EnvVar `json:"env,omitempty"` + Command []string `json:"command,omitempty"` + Args []string `json:"args,omitempty"` + SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"` +} + +type ACLConfig struct { + Secret *corev1.SecretVolumeSource `json:"secret,omitempty"` +} diff --git a/pkg/vendored/redis-operator/common/v1beta2/common_types_test.go b/pkg/vendored/redis-operator/common/v1beta2/common_types_test.go new file mode 100644 index 00000000..141c6385 --- /dev/null +++ b/pkg/vendored/redis-operator/common/v1beta2/common_types_test.go @@ -0,0 +1,86 @@ +package v1beta2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/utils/ptr" +) + +func TestKubernetesConfig_ShouldCreateAdditionalService(t *testing.T) { + tests := []struct { + name string + config *KubernetesConfig + expected bool + }{ + { + name: "nil config", + config: nil, + expected: true, + }, + { + name: "empty config", + config: &KubernetesConfig{}, + expected: true, + }, + { + name: "nil service", + config: &KubernetesConfig{ + Service: nil, + }, + expected: true, + }, + { + name: "nil additional", + config: &KubernetesConfig{ + Service: &ServiceConfig{ + Additional: nil, + }, + }, + expected: true, + }, + { + name: "nil enabled", + config: &KubernetesConfig{ + Service: &ServiceConfig{ + Additional: &Service{ + Enabled: nil, + }, + }, + }, + expected: true, + }, + { + name: "enabled true", + config: &KubernetesConfig{ + Service: &ServiceConfig{ + Additional: &Service{ + Enabled: ptr.To(true), + }, + }, + }, + expected: true, + }, + { + name: "enabled false", + config: &KubernetesConfig{ + Service: &ServiceConfig{ + Additional: &Service{ + Enabled: ptr.To(false), + }, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.config == nil { + assert.True(t, (&KubernetesConfig{}).ShouldCreateAdditionalService()) + return + } + assert.Equal(t, tt.expected, tt.config.ShouldCreateAdditionalService()) + }) + } +} diff --git a/pkg/vendored/redis-operator/common/v1beta2/doc.go b/pkg/vendored/redis-operator/common/v1beta2/doc.go new file mode 100644 index 00000000..af853188 --- /dev/null +++ b/pkg/vendored/redis-operator/common/v1beta2/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2020 Opstree Solutions. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta2 contains common types used by Redis Operator APIs. +// These types are shared across different Redis resource types. +package v1beta2 diff --git a/pkg/vendored/redis-operator/common/v1beta2/zz_generated.deepcopy.go b/pkg/vendored/redis-operator/common/v1beta2/zz_generated.deepcopy.go new file mode 100644 index 00000000..688e9201 --- /dev/null +++ b/pkg/vendored/redis-operator/common/v1beta2/zz_generated.deepcopy.go @@ -0,0 +1,630 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta2 + +import ( + appsv1 "k8s.io/api/apps/v1" + "k8s.io/api/core/v1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ACLConfig) DeepCopyInto(out *ACLConfig) { + *out = *in + if in.Secret != nil { + in, out := &in.Secret, &out.Secret + *out = new(v1.SecretVolumeSource) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ACLConfig. +func (in *ACLConfig) DeepCopy() *ACLConfig { + if in == nil { + return nil + } + out := new(ACLConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdditionalVolume) DeepCopyInto(out *AdditionalVolume) { + *out = *in + if in.Volume != nil { + in, out := &in.Volume, &out.Volume + *out = make([]v1.Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.MountPath != nil { + in, out := &in.MountPath, &out.MountPath + *out = make([]v1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalVolume. +func (in *AdditionalVolume) DeepCopy() *AdditionalVolume { + if in == nil { + return nil + } + out := new(AdditionalVolume) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExistingPasswordSecret) DeepCopyInto(out *ExistingPasswordSecret) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Key != nil { + in, out := &in.Key, &out.Key + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExistingPasswordSecret. +func (in *ExistingPasswordSecret) DeepCopy() *ExistingPasswordSecret { + if in == nil { + return nil + } + out := new(ExistingPasswordSecret) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InitContainer) DeepCopyInto(out *InitContainer) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.EnvVars != nil { + in, out := &in.EnvVars, &out.EnvVars + *out = new([]v1.EnvVar) + if **in != nil { + in, out := *in, *out + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.Command != nil { + in, out := &in.Command, &out.Command + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(v1.SecurityContext) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InitContainer. +func (in *InitContainer) DeepCopy() *InitContainer { + if in == nil { + return nil + } + out := new(InitContainer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesConfig) DeepCopyInto(out *KubernetesConfig) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.ExistingPasswordSecret != nil { + in, out := &in.ExistingPasswordSecret, &out.ExistingPasswordSecret + *out = new(ExistingPasswordSecret) + (*in).DeepCopyInto(*out) + } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = new([]v1.LocalObjectReference) + if **in != nil { + in, out := *in, *out + *out = make([]v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } + } + in.UpdateStrategy.DeepCopyInto(&out.UpdateStrategy) + if in.PersistentVolumeClaimRetentionPolicy != nil { + in, out := &in.PersistentVolumeClaimRetentionPolicy, &out.PersistentVolumeClaimRetentionPolicy + *out = new(appsv1.StatefulSetPersistentVolumeClaimRetentionPolicy) + **out = **in + } + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(ServiceConfig) + (*in).DeepCopyInto(*out) + } + if in.IgnoreAnnotations != nil { + in, out := &in.IgnoreAnnotations, &out.IgnoreAnnotations + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.MinReadySeconds != nil { + in, out := &in.MinReadySeconds, &out.MinReadySeconds + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesConfig. +func (in *KubernetesConfig) DeepCopy() *KubernetesConfig { + if in == nil { + return nil + } + out := new(KubernetesConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisConfig) DeepCopyInto(out *RedisConfig) { + *out = *in + if in.MaxMemoryPercentOfLimit != nil { + in, out := &in.MaxMemoryPercentOfLimit, &out.MaxMemoryPercentOfLimit + *out = new(int) + **out = **in + } + if in.DynamicConfig != nil { + in, out := &in.DynamicConfig, &out.DynamicConfig + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AdditionalRedisConfig != nil { + in, out := &in.AdditionalRedisConfig, &out.AdditionalRedisConfig + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisConfig. +func (in *RedisConfig) DeepCopy() *RedisConfig { + if in == nil { + return nil + } + out := new(RedisConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisExporter) DeepCopyInto(out *RedisExporter) { + *out = *in + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(int) + **out = **in + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.EnvVars != nil { + in, out := &in.EnvVars, &out.EnvVars + *out = new([]v1.EnvVar) + if **in != nil { + in, out := *in, *out + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(v1.SecurityContext) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisExporter. +func (in *RedisExporter) DeepCopy() *RedisExporter { + if in == nil { + return nil + } + out := new(RedisExporter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisFollower) DeepCopyInto(out *RedisFollower) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.RedisConfig != nil { + in, out := &in.RedisConfig, &out.RedisConfig + *out = new(RedisConfig) + (*in).DeepCopyInto(*out) + } + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(v1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.PodDisruptionBudget != nil { + in, out := &in.PodDisruptionBudget, &out.PodDisruptionBudget + *out = new(RedisPodDisruptionBudget) + (*in).DeepCopyInto(*out) + } + if in.ReadinessProbe != nil { + in, out := &in.ReadinessProbe, &out.ReadinessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.LivenessProbe != nil { + in, out := &in.LivenessProbe, &out.LivenessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = new([]v1.Toleration) + if **in != nil { + in, out := *in, *out + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.TopologySpreadConstraints != nil { + in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints + *out = make([]v1.TopologySpreadConstraint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisFollower. +func (in *RedisFollower) DeepCopy() *RedisFollower { + if in == nil { + return nil + } + out := new(RedisFollower) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisLeader) DeepCopyInto(out *RedisLeader) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.RedisConfig != nil { + in, out := &in.RedisConfig, &out.RedisConfig + *out = new(RedisConfig) + (*in).DeepCopyInto(*out) + } + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(v1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.PodDisruptionBudget != nil { + in, out := &in.PodDisruptionBudget, &out.PodDisruptionBudget + *out = new(RedisPodDisruptionBudget) + (*in).DeepCopyInto(*out) + } + if in.ReadinessProbe != nil { + in, out := &in.ReadinessProbe, &out.ReadinessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.LivenessProbe != nil { + in, out := &in.LivenessProbe, &out.LivenessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = new([]v1.Toleration) + if **in != nil { + in, out := *in, *out + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.TopologySpreadConstraints != nil { + in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints + *out = make([]v1.TopologySpreadConstraint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisLeader. +func (in *RedisLeader) DeepCopy() *RedisLeader { + if in == nil { + return nil + } + out := new(RedisLeader) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisPodDisruptionBudget) DeepCopyInto(out *RedisPodDisruptionBudget) { + *out = *in + if in.MinAvailable != nil { + in, out := &in.MinAvailable, &out.MinAvailable + *out = new(int32) + **out = **in + } + if in.MaxUnavailable != nil { + in, out := &in.MaxUnavailable, &out.MaxUnavailable + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisPodDisruptionBudget. +func (in *RedisPodDisruptionBudget) DeepCopy() *RedisPodDisruptionBudget { + if in == nil { + return nil + } + out := new(RedisPodDisruptionBudget) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSentinelConfig) DeepCopyInto(out *RedisSentinelConfig) { + *out = *in + if in.AdditionalSentinelConfig != nil { + in, out := &in.AdditionalSentinelConfig, &out.AdditionalSentinelConfig + *out = new(string) + **out = **in + } + if in.RedisReplicationPassword != nil { + in, out := &in.RedisReplicationPassword, &out.RedisReplicationPassword + *out = new(v1.EnvVarSource) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSentinelConfig. +func (in *RedisSentinelConfig) DeepCopy() *RedisSentinelConfig { + if in == nil { + return nil + } + out := new(RedisSentinelConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Service) DeepCopyInto(out *Service) { + *out = *in + if in.AdditionalAnnotations != nil { + in, out := &in.AdditionalAnnotations, &out.AdditionalAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.IncludeBusPort != nil { + in, out := &in.IncludeBusPort, &out.IncludeBusPort + *out = new(bool) + **out = **in + } + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Service. +func (in *Service) DeepCopy() *Service { + if in == nil { + return nil + } + out := new(Service) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceConfig) DeepCopyInto(out *ServiceConfig) { + *out = *in + if in.ServiceAnnotations != nil { + in, out := &in.ServiceAnnotations, &out.ServiceAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.IncludeBusPort != nil { + in, out := &in.IncludeBusPort, &out.IncludeBusPort + *out = new(bool) + **out = **in + } + if in.Headless != nil { + in, out := &in.Headless, &out.Headless + *out = new(Service) + (*in).DeepCopyInto(*out) + } + if in.Additional != nil { + in, out := &in.Additional, &out.Additional + *out = new(Service) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceConfig. +func (in *ServiceConfig) DeepCopy() *ServiceConfig { + if in == nil { + return nil + } + out := new(ServiceConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Sidecar) DeepCopyInto(out *Sidecar) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.EnvVars != nil { + in, out := &in.EnvVars, &out.EnvVars + *out = new([]v1.EnvVar) + if **in != nil { + in, out := *in, *out + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = new([]v1.VolumeMount) + if **in != nil { + in, out := *in, *out + *out = make([]v1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.Command != nil { + in, out := &in.Command, &out.Command + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Ports != nil { + in, out := &in.Ports, &out.Ports + *out = new([]v1.ContainerPort) + if **in != nil { + in, out := *in, *out + *out = make([]v1.ContainerPort, len(*in)) + copy(*out, *in) + } + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(v1.SecurityContext) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Sidecar. +func (in *Sidecar) DeepCopy() *Sidecar { + if in == nil { + return nil + } + out := new(Sidecar) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Storage) DeepCopyInto(out *Storage) { + *out = *in + in.VolumeClaimTemplate.DeepCopyInto(&out.VolumeClaimTemplate) + in.VolumeMount.DeepCopyInto(&out.VolumeMount) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Storage. +func (in *Storage) DeepCopy() *Storage { + if in == nil { + return nil + } + out := new(Storage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSConfig) DeepCopyInto(out *TLSConfig) { + *out = *in + in.Secret.DeepCopyInto(&out.Secret) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSConfig. +func (in *TLSConfig) DeepCopy() *TLSConfig { + if in == nil { + return nil + } + out := new(TLSConfig) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redis.yaml b/pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redis.yaml new file mode 100644 index 00000000..dc080824 --- /dev/null +++ b/pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redis.yaml @@ -0,0 +1,5392 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: redis.redis.redis.opstreelabs.in +spec: + group: redis.redis.opstreelabs.in + names: + kind: Redis + listKind: RedisList + plural: redis + singular: redis + scope: Namespaced + versions: + - name: v1beta2 + schema: + openAPIV3Schema: + description: Redis is the Schema for the redis API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: RedisSpec defines the desired state of Redis + properties: + TLS: + description: TLS Configuration for redis instances + properties: + ca: + type: string + cert: + type: string + key: + type: string + secret: + description: Reference to secret which contains the certificates + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + required: + - secret + type: object + acl: + properties: + secret: + description: |- + Adapts a Secret into a volume. + + The contents of the target Secret's Data field will be presented in a volume + as files using the keys in the Data field as the file names. + Secret volumes support ownership management and SELinux relabeling. + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + type: object + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the + pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + env: + items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. Cannot + be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + hostPort: + type: integer + initContainer: + description: InitContainer for each Redis pods + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + kubernetesConfig: + description: KubernetesConfig will be the JSON struct for Basic Redis + Config + properties: + ignoreAnnotations: + items: + type: string + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + imagePullSecrets: + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + minReadySeconds: + format: int32 + type: integer + persistentVolumeClaimRetentionPolicy: + description: |- + StatefulSetPersistentVolumeClaimRetentionPolicy describes the policy used for PVCs + created from the StatefulSet VolumeClaimTemplates. + properties: + whenDeleted: + description: |- + WhenDeleted specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is deleted. The default policy + of `Retain` causes PVCs to not be affected by StatefulSet deletion. The + `Delete` policy causes those PVCs to be deleted. + type: string + whenScaled: + description: |- + WhenScaled specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is scaled down. The default + policy of `Retain` causes PVCs to not be affected by a scaledown. The + `Delete` policy causes the associated PVCs for any excess pods above + the replica count to be deleted. + type: string + type: object + redisSecret: + description: ExistingPasswordSecret is the struct to access the + existing secret + properties: + key: + type: string + name: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + service: + description: ServiceConfig define the type of service to be created + and its annotations + properties: + additional: + description: Additional config for which suffix is -additional + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + annotations: + additionalProperties: + type: string + type: object + headless: + description: Headless config for which suffix is -headless + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + serviceType: + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + updateStrategy: + description: |- + StatefulSetUpdateStrategy indicates the strategy that the StatefulSet + controller will use to perform updates. It includes any additional parameters + necessary to perform the update for the indicated strategy. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be unavailable during the update. + Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + Absolute number is calculated from percentage by rounding up. This can not be 0. + Defaults to 1. This field is alpha-level and is only honored by servers that enable the + MaxUnavailableStatefulSet feature. The field applies to all pods in the range 0 to + Replicas-1. That means if there is any unavailable pod in the range 0 to Replicas-1, it + will be counted towards MaxUnavailable. + x-kubernetes-int-or-string: true + partition: + description: |- + Partition indicates the ordinal at which the StatefulSet should be partitioned + for updates. During a rolling update, all pods from ordinal Replicas-1 to + Partition are updated. All pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: |- + Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object + required: + - image + type: object + livenessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + nodeSelector: + additionalProperties: + type: string + type: object + podSecurityContext: + description: |- + PodSecurityContext holds pod-level security attributes and common container settings. + Some fields are also present in container.securityContext. Field values of + container.securityContext take precedence over field values of PodSecurityContext. + properties: + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + priorityClassName: + type: string + readinessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + redisConfig: + description: RedisConfig defines the external configuration of Redis + properties: + additionalRedisConfig: + type: string + dynamicConfig: + items: + type: string + type: array + maxMemoryPercentOfLimit: + description: MaxMemoryPercentOfLimit is the percentage of redis + container memory limit to be used as maxmemory. + maximum: 100 + minimum: 1 + type: integer + type: object + redisExporter: + description: RedisExporter interface will have the information for + redis exporter related stuff + properties: + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + port: + default: 9121 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + serviceAccountName: + type: string + sidecars: + items: + description: Sidecar for each Redis pods + properties: + command: + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + name: + type: string + ports: + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + - name + type: object + type: array + storage: + description: Storage is the inteface to add pvc and pv support in + redis + properties: + keepAfterDelete: + type: boolean + volumeClaimTemplate: + description: PersistentVolumeClaim is a user's request for and + claim to a persistent volume + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + description: |- + Standard object's metadata. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + type: object + spec: + description: |- + spec defines the desired characteristics of a volume requested by a pod author. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over volumes to + consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference to the + PersistentVolume backing this claim. + type: string + type: object + status: + description: |- + status represents the current information/status of a persistent volume claim. + Read-only. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the actual access modes the volume backing the PVC has. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + allocatedResourceStatuses: + additionalProperties: + description: |- + When a controller receives persistentvolume claim update with ClaimResourceStatus for a resource + that it does not recognizes, then it should ignore that update and let other controllers + handle it. + type: string + description: "allocatedResourceStatuses stores status + of resource being resized for the given PVC.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nClaimResourceStatus can be in any + of following states:\n\t- ControllerResizeInProgress:\n\t\tState + set when resize controller starts resizing the volume + in control-plane.\n\t- ControllerResizeFailed:\n\t\tState + set when resize has failed in resize controller with + a terminal error.\n\t- NodeResizePending:\n\t\tState + set when resize controller has finished resizing the + volume but further resizing of\n\t\tvolume is needed + on the node.\n\t- NodeResizeInProgress:\n\t\tState set + when kubelet starts resizing the volume.\n\t- NodeResizeFailed:\n\t\tState + set when resizing has failed in kubelet with a terminal + error. Transient errors don't set\n\t\tNodeResizeFailed.\nFor + example: if expanding a PVC for more capacity - this + field can be one of the following states:\n\t- pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeFailed\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizePending\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeFailed\"\nWhen this field is not set, + it means that no resize operation is in progress for + the given PVC.\n\nA controller that receives PVC update + with previously unknown resourceName or ClaimResourceStatus\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + x-kubernetes-map-type: granular + allocatedResources: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: "allocatedResources tracks the resources + allocated to a PVC including its capacity.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nCapacity reported here may be larger + than the actual capacity when a volume expansion operation\nis + requested.\nFor storage quota, the larger value from + allocatedResources and PVC.spec.resources is used.\nIf + allocatedResources is not set, PVC.spec.resources alone + is used for quota calculation.\nIf a volume expansion + capacity request is lowered, allocatedResources is only\nlowered + if there are no expansion operations in progress and + if the actual volume capacity\nis equal or lower than + the requested capacity.\n\nA controller that receives + PVC update with previously unknown resourceName\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + capacity: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: capacity represents the actual resources + of the underlying volume. + type: object + conditions: + description: |- + conditions is the current Condition of persistent volume claim. If underlying persistent volume is being + resized then the Condition will be set to 'ResizeStarted'. + items: + description: PersistentVolumeClaimCondition contains + details about state of pvc + properties: + lastProbeTime: + description: lastProbeTime is the time we probed + the condition. + format: date-time + type: string + lastTransitionTime: + description: lastTransitionTime is the time the + condition transitioned from one status to another. + format: date-time + type: string + message: + description: message is the human-readable message + indicating details about last transition. + type: string + reason: + description: |- + reason is a unique, this should be a short, machine understandable string that gives the reason + for condition's last transition. If it reports "ResizeStarted" that means the underlying + persistent volume is being resized. + type: string + status: + type: string + type: + description: PersistentVolumeClaimConditionType + is a valid value of PersistentVolumeClaimCondition.Type + type: string + required: + - status + - type + type: object + type: array + currentVolumeAttributesClassName: + description: |- + currentVolumeAttributesClassName is the current name of the VolumeAttributesClass the PVC is using. + When unset, there is no VolumeAttributeClass applied to this PersistentVolumeClaim + This is an alpha field and requires enabling VolumeAttributesClass feature. + type: string + modifyVolumeStatus: + description: |- + ModifyVolumeStatus represents the status object of ControllerModifyVolume operation. + When this is unset, there is no ModifyVolume operation being attempted. + This is an alpha field and requires enabling VolumeAttributesClass feature. + properties: + status: + description: "status is the status of the ControllerModifyVolume + operation. It can be in any of following states:\n + - Pending\n Pending indicates that the PersistentVolumeClaim + cannot be modified due to unmet requirements, such + as\n the specified VolumeAttributesClass not existing.\n + - InProgress\n InProgress indicates that the volume + is being modified.\n - Infeasible\n Infeasible + indicates that the request has been rejected as + invalid by the CSI driver. To\n\t resolve the error, + a valid VolumeAttributesClass needs to be specified.\nNote: + New statuses can be added in the future. Consumers + should check for unknown statuses and fail appropriately." + type: string + targetVolumeAttributesClassName: + description: targetVolumeAttributesClassName is the + name of the VolumeAttributesClass the PVC currently + being reconciled + type: string + required: + - status + type: object + phase: + description: phase represents the current phase of PersistentVolumeClaim. + type: string + type: object + type: object + volumeMount: + description: Additional Volume is provided by user that is mounted + on the pods + properties: + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + volume: + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk + mount on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk + in the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in + the blob storage + type: string + fsType: + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage account Managed: azure + managed data disk (only in managed availability + set). defaults to shared' + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the + host that shares a pod's lifetime + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + items: + type: string + type: array + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default + is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: boolean + secretFile: + description: |- + secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + secretRef: + description: |- + secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: |- + defaultMode is optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers (Beta feature). + properties: + driver: + description: |- + driver is the name of the CSI driver that handles this volume. + Consult with your admin for the correct name as registered in the cluster. + type: string + fsType: + description: |- + fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated CSI driver + which will determine the default filesystem to apply. + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no secret is required. If the + secret object contains more than one secret, all secret references are passed. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. Consult your driver's documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about + the pod that should populate this volume + properties: + defaultMode: + description: |- + Optional: mode bits to use on created files by default. Must be a + Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name + and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: |- + ephemeral represents a volume that is handled by a cluster storage driver. + The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, + and deleted when the pod is removed. + + Use this if: + a) the volume is only needed while the pod runs, + b) features of normal volumes like restoring from snapshot or capacity + tracking are needed, + c) the storage driver is specified through a storage class, and + d) the storage driver supports dynamic volume provisioning through + a PersistentVolumeClaim (see EphemeralVolumeSource for more + information on the connection between this volume type + and PersistentVolumeClaim). + + Use PersistentVolumeClaim or one of the vendor-specific + APIs for volumes that persist for longer than the lifecycle + of an individual pod. + + Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to + be used that way - see the documentation of the driver for + more information. + + A pod can use both types of ephemeral volumes and + persistent volumes at the same time. + properties: + volumeClaimTemplate: + description: |- + Will be used to create a stand-alone PVC to provision the volume. + The pod in which this EphemeralVolumeSource is embedded will be the + owner of the PVC, i.e. the PVC will be deleted together with the + pod. The name of the PVC will be `<pod name>-<volume name>` where + `<volume name>` is the name from the `PodSpec.Volumes` array + entry. Pod validation will reject the pod if the concatenated name + is not valid for a PVC (for example, too long). + + An existing PVC with that name that is not owned by the pod + will *not* be used for the pod to avoid using an unrelated + volume by mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created PVC is + meant to be used by the pod, the PVC has to updated with an + owner reference to the pod once the pod exists. Normally + this should not be necessary, but it may be useful when + manually reconstructing a broken cluster. + + This field is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. + + Required, must not be nil. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. No other fields are allowed and will be rejected during + validation. + type: object + spec: + description: |- + The specification for the PersistentVolumeClaim. The entire content is + copied unchanged into the PVC that gets created from this + template. The same fields as in a PersistentVolumeClaim + are also valid here. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource + that is attached to a kubelet's host machine and then + exposed to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target + worldwide names (WWNs)' + items: + type: string + type: array + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. + items: + type: string + type: array + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to + use for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the plugin scripts. This may be + empty if no secret object is specified. If the secret object + contains more than one secret, all secrets are passed to the plugin + scripts. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. This depends on the Flocker + control service being running + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + properties: + fsType: + description: |- + fsType is filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an + EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir + into the Pod's container. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '.' is supplied, the volume directory will be the + git repository. Otherwise, if specified, the volume will contain the git repository in + the subdirectory with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the + specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: |- + glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/glusterfs/README.md + properties: + endpoints: + description: |- + endpoints is the endpoint name that details Glusterfs topology. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. This is generally + used for system agents or other privileged things that are allowed + to see the host machine. Most containers will NOT need this. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://examples.k8s.io/volumes/iscsi/README.md + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi + type: string + initiatorName: + description: |- + initiatorName is the custom iSCSI Initiator Name. + If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + <target portal>:<volume name> will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: |- + portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: |- + targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon + Controller persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources + secrets, configmaps, and downward API + properties: + defaultMode: + description: |- + defaultMode are the mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: sources is the list of volume projections + items: + description: Projection that may be projected + along with other supported volume types + properties: + clusterTrustBundle: + description: |- + ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field + of ClusterTrustBundle objects in an auto-updating file. + + Alpha, gated by the ClusterTrustBundleProjection feature gate. + + ClusterTrustBundle objects can either be selected by name, or by the + combination of signer name and a label selector. + + Kubelet performs aggressive normalization of the PEM contents written + into the pod filesystem. Esoteric PEM features such as inter-block + comments and block headers are stripped. Certificates are deduplicated. + The ordering of certificates within the file is arbitrary, and Kubelet + may change the order over time. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. Mutually-exclusive with name. If unset, + interpreted as "match nothing". If set but empty, interpreted as "match + everything". + properties: + matchExpressions: + description: matchExpressions is a + list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. If using name, then the named ClusterTrustBundle is + allowed not to exist. If using signerName, then the combination of + signerName and labelSelector is allowed to match zero + ClusterTrustBundles. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. The contents of all selected + ClusterTrustBundles will be unified and deduplicated. + type: string + required: + - path + type: object + configMap: + description: configMap information about the + configMap data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether + the ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about + the downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects + a field of the pod: only annotations, + labels, name and namespace are + supported.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file + to be created. Must not be absolute + or contain the ''..'' path. Must + be utf-8 encoded. The first item + of the relative path must not + start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + secret: + description: secret information about the + secret data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: |- + audience is the intended audience of the token. A recipient of a token + must identify itself with an identifier specified in the audience of the + token, and otherwise should reject the token. The audience defaults to the + identifier of the apiserver. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. As the token approaches expiration, the kubelet volume + plugin will proactively rotate the service account token. The kubelet will + start trying to rotate the token if the token is older than 80 percent of + its time to live or if the token is older than 24 hours.Defaults to 1 hour + and must be at least 10 minutes. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + description: quobyte represents a Quobyte mount on the + host that shares a pod's lifetime + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple entries are separated with commas) + which acts as the central registry for volumes + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set by the plugin + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references + an already created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: |- + rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/rbd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + pool: + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent + volume attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". + Default is "xfs". + type: string + gateway: + description: gateway is the host address of the + ScaleIO API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the + ScaleIO Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. If this is not provided, Login operation will fail. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL + communication with Gateway, default false + type: boolean + storageMode: + description: |- + storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage + Pool associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the + Secret or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. If not specified, default values will be attempted. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: |- + volumeNamespace specifies the scope of the volume within StorageOS. If no + namespace is specified then the Pod's namespace will be used. This allows the + Kubernetes name scoping to be mirrored within StorageOS for tighter integration. + Set VolumeName to any name to override the default behaviour. + Set to "default" if you are not using namespaces within StorageOS. + Namespaces that do not pre-exist within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy + Based Management (SPBM) profile ID associated + with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple <key,value,effect> using the matching operator <operator>. + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + required: + - kubernetesConfig + type: object + status: + description: RedisStatus defines the observed state of Redis + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redisclusters.yaml b/pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redisclusters.yaml new file mode 100644 index 00000000..aade043f --- /dev/null +++ b/pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redisclusters.yaml @@ -0,0 +1,7862 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: redisclusters.redis.redis.opstreelabs.in +spec: + group: redis.redis.opstreelabs.in + names: + kind: RedisCluster + listKind: RedisClusterList + plural: redisclusters + singular: rediscluster + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Current cluster node count + jsonPath: .spec.clusterSize + name: ClusterSize + type: integer + - description: Number of ready leader replicas + jsonPath: .status.readyLeaderReplicas + name: ReadyLeaderReplicas + type: integer + - description: Number of ready follower replicas + jsonPath: .status.readyFollowerReplicas + name: ReadyFollowerReplicas + type: integer + - description: The current state of the Redis Cluster + jsonPath: .status.state + name: State + priority: 1 + type: string + - description: Age of Cluster + jsonPath: .metadata.creationTimestamp + name: Age + priority: 1 + type: date + - description: The reason for the current state + jsonPath: .status.reason + name: Reason + priority: 1 + type: string + name: v1beta2 + schema: + openAPIV3Schema: + description: RedisCluster is the Schema for the redisclusters API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: RedisClusterSpec defines the desired state of RedisCluster + properties: + TLS: + description: TLS Configuration for redis instances + properties: + ca: + type: string + cert: + type: string + key: + type: string + secret: + description: Reference to secret which contains the certificates + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + required: + - secret + type: object + acl: + properties: + secret: + description: |- + Adapts a Secret into a volume. + + The contents of the target Secret's Data field will be presented in a volume + as files using the keys in the Data field as the file names. + Secret volumes support ownership management and SELinux relabeling. + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + type: object + clusterSize: + description: ClusterSize defines the default number of replicas for + both leader and follower when not explicitly set + format: int32 + type: integer + clusterVersion: + default: v7 + type: string + env: + items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. Cannot + be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + hostNetwork: + type: boolean + hostPort: + type: integer + initContainer: + description: InitContainer for each Redis pods + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + kubernetesConfig: + description: KubernetesConfig will be the JSON struct for Basic Redis + Config + properties: + ignoreAnnotations: + items: + type: string + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + imagePullSecrets: + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + minReadySeconds: + format: int32 + type: integer + persistentVolumeClaimRetentionPolicy: + description: |- + StatefulSetPersistentVolumeClaimRetentionPolicy describes the policy used for PVCs + created from the StatefulSet VolumeClaimTemplates. + properties: + whenDeleted: + description: |- + WhenDeleted specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is deleted. The default policy + of `Retain` causes PVCs to not be affected by StatefulSet deletion. The + `Delete` policy causes those PVCs to be deleted. + type: string + whenScaled: + description: |- + WhenScaled specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is scaled down. The default + policy of `Retain` causes PVCs to not be affected by a scaledown. The + `Delete` policy causes the associated PVCs for any excess pods above + the replica count to be deleted. + type: string + type: object + redisSecret: + description: ExistingPasswordSecret is the struct to access the + existing secret + properties: + key: + type: string + name: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + service: + description: ServiceConfig define the type of service to be created + and its annotations + properties: + additional: + description: Additional config for which suffix is -additional + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + annotations: + additionalProperties: + type: string + type: object + headless: + description: Headless config for which suffix is -headless + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + serviceType: + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + updateStrategy: + description: |- + StatefulSetUpdateStrategy indicates the strategy that the StatefulSet + controller will use to perform updates. It includes any additional parameters + necessary to perform the update for the indicated strategy. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be unavailable during the update. + Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + Absolute number is calculated from percentage by rounding up. This can not be 0. + Defaults to 1. This field is alpha-level and is only honored by servers that enable the + MaxUnavailableStatefulSet feature. The field applies to all pods in the range 0 to + Replicas-1. That means if there is any unavailable pod in the range 0 to Replicas-1, it + will be counted towards MaxUnavailable. + x-kubernetes-int-or-string: true + partition: + description: |- + Partition indicates the ordinal at which the StatefulSet should be partitioned + for updates. During a rolling update, all pods from ordinal Replicas-1 to + Partition are updated. All pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: |- + Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object + required: + - image + type: object + persistenceEnabled: + type: boolean + podSecurityContext: + description: |- + PodSecurityContext holds pod-level security attributes and common container settings. + Some fields are also present in container.securityContext. Field values of + container.securityContext take precedence over field values of PodSecurityContext. + properties: + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + port: + default: 6379 + type: integer + priorityClassName: + type: string + redisConfig: + description: RedisConfig defines the external configuration of Redis + properties: + additionalRedisConfig: + type: string + dynamicConfig: + items: + type: string + type: array + maxMemoryPercentOfLimit: + description: MaxMemoryPercentOfLimit is the percentage of redis + container memory limit to be used as maxmemory. + maximum: 100 + minimum: 1 + type: integer + type: object + redisExporter: + description: RedisExporter interface will have the information for + redis exporter related stuff + properties: + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + port: + default: 9121 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + redisFollower: + description: RedisFollower interface will have the redis follower + configuration + properties: + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + livenessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + nodeSelector: + additionalProperties: + type: string + type: object + pdb: + description: RedisPodDisruptionBudget configure a PodDisruptionBudget + on the resource (leader/follower) + properties: + enabled: + type: boolean + maxUnavailable: + format: int32 + type: integer + minAvailable: + format: int32 + type: integer + type: object + readinessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + redisConfig: + description: RedisConfig defines the external configuration of + Redis + properties: + additionalRedisConfig: + type: string + dynamicConfig: + items: + type: string + type: array + maxMemoryPercentOfLimit: + description: MaxMemoryPercentOfLimit is the percentage of + redis container memory limit to be used as maxmemory. + maximum: 100 + minimum: 1 + type: integer + type: object + replicas: + description: Replicas overrides clusterSize for follower nodes + count. If not set, uses clusterSize value + format: int32 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple <key,value,effect> using the matching operator <operator>. + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 + type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. + + This is a beta field and requires the MinDomainsInPodTopologySpread feature gate to be enabled (enabled by default). + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + If this value is nil, the behavior is equivalent to the Honor policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + If this value is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each <key, value> as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + type: object + redisLeader: + description: RedisLeader interface will have the redis leader configuration + properties: + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + livenessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + nodeSelector: + additionalProperties: + type: string + type: object + pdb: + description: RedisPodDisruptionBudget configure a PodDisruptionBudget + on the resource (leader/follower) + properties: + enabled: + type: boolean + maxUnavailable: + format: int32 + type: integer + minAvailable: + format: int32 + type: integer + type: object + readinessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP + allows repeated headers. + items: + description: HTTPHeader describes a custom header to + be used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP + port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + redisConfig: + description: RedisConfig defines the external configuration of + Redis + properties: + additionalRedisConfig: + type: string + dynamicConfig: + items: + type: string + type: array + maxMemoryPercentOfLimit: + description: MaxMemoryPercentOfLimit is the percentage of + redis container memory limit to be used as maxmemory. + maximum: 100 + minimum: 1 + type: integer + type: object + replicas: + description: Replicas overrides clusterSize for leader nodes count. + If not set, uses clusterSize value + format: int32 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple <key,value,effect> using the matching operator <operator>. + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 + type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. + + This is a beta field and requires the MinDomainsInPodTopologySpread feature gate to be enabled (enabled by default). + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + If this value is nil, the behavior is equivalent to the Honor policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + If this value is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each <key, value> as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + type: object + resources: + description: ResourceRequirements describes the compute resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + serviceAccountName: + type: string + sidecars: + items: + description: Sidecar for each Redis pods + properties: + command: + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + name: + type: string + ports: + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + - name + type: object + type: array + storage: + description: Node-conf needs to be added only in redis cluster + properties: + keepAfterDelete: + type: boolean + nodeConfVolume: + default: false + type: boolean + nodeConfVolumeClaimTemplate: + description: PersistentVolumeClaim is a user's request for and + claim to a persistent volume + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + description: |- + Standard object's metadata. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + type: object + spec: + description: |- + spec defines the desired characteristics of a volume requested by a pod author. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over volumes to + consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference to the + PersistentVolume backing this claim. + type: string + type: object + status: + description: |- + status represents the current information/status of a persistent volume claim. + Read-only. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the actual access modes the volume backing the PVC has. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + allocatedResourceStatuses: + additionalProperties: + description: |- + When a controller receives persistentvolume claim update with ClaimResourceStatus for a resource + that it does not recognizes, then it should ignore that update and let other controllers + handle it. + type: string + description: "allocatedResourceStatuses stores status + of resource being resized for the given PVC.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nClaimResourceStatus can be in any + of following states:\n\t- ControllerResizeInProgress:\n\t\tState + set when resize controller starts resizing the volume + in control-plane.\n\t- ControllerResizeFailed:\n\t\tState + set when resize has failed in resize controller with + a terminal error.\n\t- NodeResizePending:\n\t\tState + set when resize controller has finished resizing the + volume but further resizing of\n\t\tvolume is needed + on the node.\n\t- NodeResizeInProgress:\n\t\tState set + when kubelet starts resizing the volume.\n\t- NodeResizeFailed:\n\t\tState + set when resizing has failed in kubelet with a terminal + error. Transient errors don't set\n\t\tNodeResizeFailed.\nFor + example: if expanding a PVC for more capacity - this + field can be one of the following states:\n\t- pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeFailed\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizePending\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeFailed\"\nWhen this field is not set, + it means that no resize operation is in progress for + the given PVC.\n\nA controller that receives PVC update + with previously unknown resourceName or ClaimResourceStatus\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + x-kubernetes-map-type: granular + allocatedResources: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: "allocatedResources tracks the resources + allocated to a PVC including its capacity.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nCapacity reported here may be larger + than the actual capacity when a volume expansion operation\nis + requested.\nFor storage quota, the larger value from + allocatedResources and PVC.spec.resources is used.\nIf + allocatedResources is not set, PVC.spec.resources alone + is used for quota calculation.\nIf a volume expansion + capacity request is lowered, allocatedResources is only\nlowered + if there are no expansion operations in progress and + if the actual volume capacity\nis equal or lower than + the requested capacity.\n\nA controller that receives + PVC update with previously unknown resourceName\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + capacity: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: capacity represents the actual resources + of the underlying volume. + type: object + conditions: + description: |- + conditions is the current Condition of persistent volume claim. If underlying persistent volume is being + resized then the Condition will be set to 'ResizeStarted'. + items: + description: PersistentVolumeClaimCondition contains + details about state of pvc + properties: + lastProbeTime: + description: lastProbeTime is the time we probed + the condition. + format: date-time + type: string + lastTransitionTime: + description: lastTransitionTime is the time the + condition transitioned from one status to another. + format: date-time + type: string + message: + description: message is the human-readable message + indicating details about last transition. + type: string + reason: + description: |- + reason is a unique, this should be a short, machine understandable string that gives the reason + for condition's last transition. If it reports "ResizeStarted" that means the underlying + persistent volume is being resized. + type: string + status: + type: string + type: + description: PersistentVolumeClaimConditionType + is a valid value of PersistentVolumeClaimCondition.Type + type: string + required: + - status + - type + type: object + type: array + currentVolumeAttributesClassName: + description: |- + currentVolumeAttributesClassName is the current name of the VolumeAttributesClass the PVC is using. + When unset, there is no VolumeAttributeClass applied to this PersistentVolumeClaim + This is an alpha field and requires enabling VolumeAttributesClass feature. + type: string + modifyVolumeStatus: + description: |- + ModifyVolumeStatus represents the status object of ControllerModifyVolume operation. + When this is unset, there is no ModifyVolume operation being attempted. + This is an alpha field and requires enabling VolumeAttributesClass feature. + properties: + status: + description: "status is the status of the ControllerModifyVolume + operation. It can be in any of following states:\n + - Pending\n Pending indicates that the PersistentVolumeClaim + cannot be modified due to unmet requirements, such + as\n the specified VolumeAttributesClass not existing.\n + - InProgress\n InProgress indicates that the volume + is being modified.\n - Infeasible\n Infeasible + indicates that the request has been rejected as + invalid by the CSI driver. To\n\t resolve the error, + a valid VolumeAttributesClass needs to be specified.\nNote: + New statuses can be added in the future. Consumers + should check for unknown statuses and fail appropriately." + type: string + targetVolumeAttributesClassName: + description: targetVolumeAttributesClassName is the + name of the VolumeAttributesClass the PVC currently + being reconciled + type: string + required: + - status + type: object + phase: + description: phase represents the current phase of PersistentVolumeClaim. + type: string + type: object + type: object + volumeClaimTemplate: + description: PersistentVolumeClaim is a user's request for and + claim to a persistent volume + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + description: |- + Standard object's metadata. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + type: object + spec: + description: |- + spec defines the desired characteristics of a volume requested by a pod author. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over volumes to + consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference to the + PersistentVolume backing this claim. + type: string + type: object + status: + description: |- + status represents the current information/status of a persistent volume claim. + Read-only. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the actual access modes the volume backing the PVC has. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + allocatedResourceStatuses: + additionalProperties: + description: |- + When a controller receives persistentvolume claim update with ClaimResourceStatus for a resource + that it does not recognizes, then it should ignore that update and let other controllers + handle it. + type: string + description: "allocatedResourceStatuses stores status + of resource being resized for the given PVC.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nClaimResourceStatus can be in any + of following states:\n\t- ControllerResizeInProgress:\n\t\tState + set when resize controller starts resizing the volume + in control-plane.\n\t- ControllerResizeFailed:\n\t\tState + set when resize has failed in resize controller with + a terminal error.\n\t- NodeResizePending:\n\t\tState + set when resize controller has finished resizing the + volume but further resizing of\n\t\tvolume is needed + on the node.\n\t- NodeResizeInProgress:\n\t\tState set + when kubelet starts resizing the volume.\n\t- NodeResizeFailed:\n\t\tState + set when resizing has failed in kubelet with a terminal + error. Transient errors don't set\n\t\tNodeResizeFailed.\nFor + example: if expanding a PVC for more capacity - this + field can be one of the following states:\n\t- pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeFailed\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizePending\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeFailed\"\nWhen this field is not set, + it means that no resize operation is in progress for + the given PVC.\n\nA controller that receives PVC update + with previously unknown resourceName or ClaimResourceStatus\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + x-kubernetes-map-type: granular + allocatedResources: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: "allocatedResources tracks the resources + allocated to a PVC including its capacity.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nCapacity reported here may be larger + than the actual capacity when a volume expansion operation\nis + requested.\nFor storage quota, the larger value from + allocatedResources and PVC.spec.resources is used.\nIf + allocatedResources is not set, PVC.spec.resources alone + is used for quota calculation.\nIf a volume expansion + capacity request is lowered, allocatedResources is only\nlowered + if there are no expansion operations in progress and + if the actual volume capacity\nis equal or lower than + the requested capacity.\n\nA controller that receives + PVC update with previously unknown resourceName\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + capacity: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: capacity represents the actual resources + of the underlying volume. + type: object + conditions: + description: |- + conditions is the current Condition of persistent volume claim. If underlying persistent volume is being + resized then the Condition will be set to 'ResizeStarted'. + items: + description: PersistentVolumeClaimCondition contains + details about state of pvc + properties: + lastProbeTime: + description: lastProbeTime is the time we probed + the condition. + format: date-time + type: string + lastTransitionTime: + description: lastTransitionTime is the time the + condition transitioned from one status to another. + format: date-time + type: string + message: + description: message is the human-readable message + indicating details about last transition. + type: string + reason: + description: |- + reason is a unique, this should be a short, machine understandable string that gives the reason + for condition's last transition. If it reports "ResizeStarted" that means the underlying + persistent volume is being resized. + type: string + status: + type: string + type: + description: PersistentVolumeClaimConditionType + is a valid value of PersistentVolumeClaimCondition.Type + type: string + required: + - status + - type + type: object + type: array + currentVolumeAttributesClassName: + description: |- + currentVolumeAttributesClassName is the current name of the VolumeAttributesClass the PVC is using. + When unset, there is no VolumeAttributeClass applied to this PersistentVolumeClaim + This is an alpha field and requires enabling VolumeAttributesClass feature. + type: string + modifyVolumeStatus: + description: |- + ModifyVolumeStatus represents the status object of ControllerModifyVolume operation. + When this is unset, there is no ModifyVolume operation being attempted. + This is an alpha field and requires enabling VolumeAttributesClass feature. + properties: + status: + description: "status is the status of the ControllerModifyVolume + operation. It can be in any of following states:\n + - Pending\n Pending indicates that the PersistentVolumeClaim + cannot be modified due to unmet requirements, such + as\n the specified VolumeAttributesClass not existing.\n + - InProgress\n InProgress indicates that the volume + is being modified.\n - Infeasible\n Infeasible + indicates that the request has been rejected as + invalid by the CSI driver. To\n\t resolve the error, + a valid VolumeAttributesClass needs to be specified.\nNote: + New statuses can be added in the future. Consumers + should check for unknown statuses and fail appropriately." + type: string + targetVolumeAttributesClassName: + description: targetVolumeAttributesClassName is the + name of the VolumeAttributesClass the PVC currently + being reconciled + type: string + required: + - status + type: object + phase: + description: phase represents the current phase of PersistentVolumeClaim. + type: string + type: object + type: object + volumeMount: + description: Additional Volume is provided by user that is mounted + on the pods + properties: + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + volume: + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk + mount on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk + in the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in + the blob storage + type: string + fsType: + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage account Managed: azure + managed data disk (only in managed availability + set). defaults to shared' + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the + host that shares a pod's lifetime + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + items: + type: string + type: array + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default + is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: boolean + secretFile: + description: |- + secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + secretRef: + description: |- + secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: |- + defaultMode is optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers (Beta feature). + properties: + driver: + description: |- + driver is the name of the CSI driver that handles this volume. + Consult with your admin for the correct name as registered in the cluster. + type: string + fsType: + description: |- + fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated CSI driver + which will determine the default filesystem to apply. + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no secret is required. If the + secret object contains more than one secret, all secret references are passed. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. Consult your driver's documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about + the pod that should populate this volume + properties: + defaultMode: + description: |- + Optional: mode bits to use on created files by default. Must be a + Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name + and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: |- + ephemeral represents a volume that is handled by a cluster storage driver. + The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, + and deleted when the pod is removed. + + Use this if: + a) the volume is only needed while the pod runs, + b) features of normal volumes like restoring from snapshot or capacity + tracking are needed, + c) the storage driver is specified through a storage class, and + d) the storage driver supports dynamic volume provisioning through + a PersistentVolumeClaim (see EphemeralVolumeSource for more + information on the connection between this volume type + and PersistentVolumeClaim). + + Use PersistentVolumeClaim or one of the vendor-specific + APIs for volumes that persist for longer than the lifecycle + of an individual pod. + + Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to + be used that way - see the documentation of the driver for + more information. + + A pod can use both types of ephemeral volumes and + persistent volumes at the same time. + properties: + volumeClaimTemplate: + description: |- + Will be used to create a stand-alone PVC to provision the volume. + The pod in which this EphemeralVolumeSource is embedded will be the + owner of the PVC, i.e. the PVC will be deleted together with the + pod. The name of the PVC will be `<pod name>-<volume name>` where + `<volume name>` is the name from the `PodSpec.Volumes` array + entry. Pod validation will reject the pod if the concatenated name + is not valid for a PVC (for example, too long). + + An existing PVC with that name that is not owned by the pod + will *not* be used for the pod to avoid using an unrelated + volume by mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created PVC is + meant to be used by the pod, the PVC has to updated with an + owner reference to the pod once the pod exists. Normally + this should not be necessary, but it may be useful when + manually reconstructing a broken cluster. + + This field is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. + + Required, must not be nil. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. No other fields are allowed and will be rejected during + validation. + type: object + spec: + description: |- + The specification for the PersistentVolumeClaim. The entire content is + copied unchanged into the PVC that gets created from this + template. The same fields as in a PersistentVolumeClaim + are also valid here. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource + that is attached to a kubelet's host machine and then + exposed to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target + worldwide names (WWNs)' + items: + type: string + type: array + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. + items: + type: string + type: array + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to + use for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the plugin scripts. This may be + empty if no secret object is specified. If the secret object + contains more than one secret, all secrets are passed to the plugin + scripts. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. This depends on the Flocker + control service being running + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + properties: + fsType: + description: |- + fsType is filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an + EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir + into the Pod's container. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '.' is supplied, the volume directory will be the + git repository. Otherwise, if specified, the volume will contain the git repository in + the subdirectory with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the + specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: |- + glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/glusterfs/README.md + properties: + endpoints: + description: |- + endpoints is the endpoint name that details Glusterfs topology. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. This is generally + used for system agents or other privileged things that are allowed + to see the host machine. Most containers will NOT need this. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://examples.k8s.io/volumes/iscsi/README.md + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi + type: string + initiatorName: + description: |- + initiatorName is the custom iSCSI Initiator Name. + If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + <target portal>:<volume name> will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: |- + portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: |- + targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon + Controller persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources + secrets, configmaps, and downward API + properties: + defaultMode: + description: |- + defaultMode are the mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: sources is the list of volume projections + items: + description: Projection that may be projected + along with other supported volume types + properties: + clusterTrustBundle: + description: |- + ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field + of ClusterTrustBundle objects in an auto-updating file. + + Alpha, gated by the ClusterTrustBundleProjection feature gate. + + ClusterTrustBundle objects can either be selected by name, or by the + combination of signer name and a label selector. + + Kubelet performs aggressive normalization of the PEM contents written + into the pod filesystem. Esoteric PEM features such as inter-block + comments and block headers are stripped. Certificates are deduplicated. + The ordering of certificates within the file is arbitrary, and Kubelet + may change the order over time. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. Mutually-exclusive with name. If unset, + interpreted as "match nothing". If set but empty, interpreted as "match + everything". + properties: + matchExpressions: + description: matchExpressions is a + list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. If using name, then the named ClusterTrustBundle is + allowed not to exist. If using signerName, then the combination of + signerName and labelSelector is allowed to match zero + ClusterTrustBundles. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. The contents of all selected + ClusterTrustBundles will be unified and deduplicated. + type: string + required: + - path + type: object + configMap: + description: configMap information about the + configMap data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether + the ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about + the downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects + a field of the pod: only annotations, + labels, name and namespace are + supported.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file + to be created. Must not be absolute + or contain the ''..'' path. Must + be utf-8 encoded. The first item + of the relative path must not + start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + secret: + description: secret information about the + secret data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: |- + audience is the intended audience of the token. A recipient of a token + must identify itself with an identifier specified in the audience of the + token, and otherwise should reject the token. The audience defaults to the + identifier of the apiserver. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. As the token approaches expiration, the kubelet volume + plugin will proactively rotate the service account token. The kubelet will + start trying to rotate the token if the token is older than 80 percent of + its time to live or if the token is older than 24 hours.Defaults to 1 hour + and must be at least 10 minutes. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + description: quobyte represents a Quobyte mount on the + host that shares a pod's lifetime + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple entries are separated with commas) + which acts as the central registry for volumes + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set by the plugin + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references + an already created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: |- + rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/rbd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + pool: + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent + volume attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". + Default is "xfs". + type: string + gateway: + description: gateway is the host address of the + ScaleIO API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the + ScaleIO Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. If this is not provided, Login operation will fail. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL + communication with Gateway, default false + type: boolean + storageMode: + description: |- + storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage + Pool associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the + Secret or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. If not specified, default values will be attempted. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: |- + volumeNamespace specifies the scope of the volume within StorageOS. If no + namespace is specified then the Pod's namespace will be used. This allows the + Kubernetes name scoping to be mirrored within StorageOS for tighter integration. + Set VolumeName to any name to override the default behaviour. + Set to "default" if you are not using namespaces within StorageOS. + Namespaces that do not pre-exist within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy + Based Management (SPBM) profile ID associated + with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + type: object + required: + - clusterSize + - kubernetesConfig + type: object + status: + description: RedisClusterStatus defines the observed state of RedisCluster + properties: + readyFollowerReplicas: + default: 0 + format: int32 + type: integer + readyLeaderReplicas: + default: 0 + format: int32 + type: integer + reason: + type: string + state: + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redisreplications.yaml b/pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redisreplications.yaml new file mode 100644 index 00000000..51d2ab96 --- /dev/null +++ b/pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redisreplications.yaml @@ -0,0 +1,5593 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: redisreplications.redis.redis.opstreelabs.in +spec: + group: redis.redis.opstreelabs.in + names: + kind: RedisReplication + listKind: RedisReplicationList + plural: redisreplications + singular: redisreplication + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.masterNode + name: Master + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1beta2 + schema: + openAPIV3Schema: + description: Redis is the Schema for the redis API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + TLS: + description: TLS Configuration for redis instances + properties: + ca: + type: string + cert: + type: string + key: + type: string + secret: + description: Reference to secret which contains the certificates + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + required: + - secret + type: object + acl: + properties: + secret: + description: |- + Adapts a Secret into a volume. + + The contents of the target Secret's Data field will be presented in a volume + as files using the keys in the Data field as the file names. + Secret volumes support ownership management and SELinux relabeling. + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + type: object + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the + pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + clusterSize: + format: int32 + type: integer + env: + items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. Cannot + be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + hostPort: + type: integer + initContainer: + description: InitContainer for each Redis pods + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + kubernetesConfig: + description: KubernetesConfig will be the JSON struct for Basic Redis + Config + properties: + ignoreAnnotations: + items: + type: string + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + imagePullSecrets: + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + minReadySeconds: + format: int32 + type: integer + persistentVolumeClaimRetentionPolicy: + description: |- + StatefulSetPersistentVolumeClaimRetentionPolicy describes the policy used for PVCs + created from the StatefulSet VolumeClaimTemplates. + properties: + whenDeleted: + description: |- + WhenDeleted specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is deleted. The default policy + of `Retain` causes PVCs to not be affected by StatefulSet deletion. The + `Delete` policy causes those PVCs to be deleted. + type: string + whenScaled: + description: |- + WhenScaled specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is scaled down. The default + policy of `Retain` causes PVCs to not be affected by a scaledown. The + `Delete` policy causes the associated PVCs for any excess pods above + the replica count to be deleted. + type: string + type: object + redisSecret: + description: ExistingPasswordSecret is the struct to access the + existing secret + properties: + key: + type: string + name: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + service: + description: ServiceConfig define the type of service to be created + and its annotations + properties: + additional: + description: Additional config for which suffix is -additional + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + annotations: + additionalProperties: + type: string + type: object + headless: + description: Headless config for which suffix is -headless + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + serviceType: + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + updateStrategy: + description: |- + StatefulSetUpdateStrategy indicates the strategy that the StatefulSet + controller will use to perform updates. It includes any additional parameters + necessary to perform the update for the indicated strategy. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be unavailable during the update. + Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + Absolute number is calculated from percentage by rounding up. This can not be 0. + Defaults to 1. This field is alpha-level and is only honored by servers that enable the + MaxUnavailableStatefulSet feature. The field applies to all pods in the range 0 to + Replicas-1. That means if there is any unavailable pod in the range 0 to Replicas-1, it + will be counted towards MaxUnavailable. + x-kubernetes-int-or-string: true + partition: + description: |- + Partition indicates the ordinal at which the StatefulSet should be partitioned + for updates. During a rolling update, all pods from ordinal Replicas-1 to + Partition are updated. All pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: |- + Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object + required: + - image + type: object + livenessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + nodeSelector: + additionalProperties: + type: string + type: object + pdb: + description: RedisPodDisruptionBudget configure a PodDisruptionBudget + on the resource (leader/follower) + properties: + enabled: + type: boolean + maxUnavailable: + format: int32 + type: integer + minAvailable: + format: int32 + type: integer + type: object + podSecurityContext: + description: |- + PodSecurityContext holds pod-level security attributes and common container settings. + Some fields are also present in container.securityContext. Field values of + container.securityContext take precedence over field values of PodSecurityContext. + properties: + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + priorityClassName: + type: string + readinessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + redisConfig: + description: RedisConfig defines the external configuration of Redis + properties: + additionalRedisConfig: + type: string + dynamicConfig: + items: + type: string + type: array + maxMemoryPercentOfLimit: + description: MaxMemoryPercentOfLimit is the percentage of redis + container memory limit to be used as maxmemory. + maximum: 100 + minimum: 1 + type: integer + type: object + redisExporter: + description: RedisExporter interface will have the information for + redis exporter related stuff + properties: + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + port: + default: 9121 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + serviceAccountName: + type: string + sidecars: + items: + description: Sidecar for each Redis pods + properties: + command: + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + name: + type: string + ports: + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + - name + type: object + type: array + storage: + description: Storage is the inteface to add pvc and pv support in + redis + properties: + keepAfterDelete: + type: boolean + volumeClaimTemplate: + description: PersistentVolumeClaim is a user's request for and + claim to a persistent volume + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + description: |- + Standard object's metadata. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + type: object + spec: + description: |- + spec defines the desired characteristics of a volume requested by a pod author. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over volumes to + consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference to the + PersistentVolume backing this claim. + type: string + type: object + status: + description: |- + status represents the current information/status of a persistent volume claim. + Read-only. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + accessModes: + description: |- + accessModes contains the actual access modes the volume backing the PVC has. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + allocatedResourceStatuses: + additionalProperties: + description: |- + When a controller receives persistentvolume claim update with ClaimResourceStatus for a resource + that it does not recognizes, then it should ignore that update and let other controllers + handle it. + type: string + description: "allocatedResourceStatuses stores status + of resource being resized for the given PVC.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nClaimResourceStatus can be in any + of following states:\n\t- ControllerResizeInProgress:\n\t\tState + set when resize controller starts resizing the volume + in control-plane.\n\t- ControllerResizeFailed:\n\t\tState + set when resize has failed in resize controller with + a terminal error.\n\t- NodeResizePending:\n\t\tState + set when resize controller has finished resizing the + volume but further resizing of\n\t\tvolume is needed + on the node.\n\t- NodeResizeInProgress:\n\t\tState set + when kubelet starts resizing the volume.\n\t- NodeResizeFailed:\n\t\tState + set when resizing has failed in kubelet with a terminal + error. Transient errors don't set\n\t\tNodeResizeFailed.\nFor + example: if expanding a PVC for more capacity - this + field can be one of the following states:\n\t- pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"ControllerResizeFailed\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizePending\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeInProgress\"\n - pvc.status.allocatedResourceStatus['storage'] + = \"NodeResizeFailed\"\nWhen this field is not set, + it means that no resize operation is in progress for + the given PVC.\n\nA controller that receives PVC update + with previously unknown resourceName or ClaimResourceStatus\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + x-kubernetes-map-type: granular + allocatedResources: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: "allocatedResources tracks the resources + allocated to a PVC including its capacity.\nKey names + follow standard Kubernetes label syntax. Valid values + are either:\n\t* Un-prefixed keys:\n\t\t- storage - + the capacity of the volume.\n\t* Custom resources must + use implementation-defined prefixed names such as \"example.com/my-custom-resource\"\nApart + from above values - keys that are unprefixed or have + kubernetes.io prefix are considered\nreserved and hence + may not be used.\n\nCapacity reported here may be larger + than the actual capacity when a volume expansion operation\nis + requested.\nFor storage quota, the larger value from + allocatedResources and PVC.spec.resources is used.\nIf + allocatedResources is not set, PVC.spec.resources alone + is used for quota calculation.\nIf a volume expansion + capacity request is lowered, allocatedResources is only\nlowered + if there are no expansion operations in progress and + if the actual volume capacity\nis equal or lower than + the requested capacity.\n\nA controller that receives + PVC update with previously unknown resourceName\nshould + ignore the update for the purpose it was designed. For + example - a controller that\nonly is responsible for + resizing capacity of the volume, should ignore PVC updates + that change other valid\nresources associated with PVC.\n\nThis + is an alpha field and requires enabling RecoverVolumeExpansionFailure + feature." + type: object + capacity: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: capacity represents the actual resources + of the underlying volume. + type: object + conditions: + description: |- + conditions is the current Condition of persistent volume claim. If underlying persistent volume is being + resized then the Condition will be set to 'ResizeStarted'. + items: + description: PersistentVolumeClaimCondition contains + details about state of pvc + properties: + lastProbeTime: + description: lastProbeTime is the time we probed + the condition. + format: date-time + type: string + lastTransitionTime: + description: lastTransitionTime is the time the + condition transitioned from one status to another. + format: date-time + type: string + message: + description: message is the human-readable message + indicating details about last transition. + type: string + reason: + description: |- + reason is a unique, this should be a short, machine understandable string that gives the reason + for condition's last transition. If it reports "ResizeStarted" that means the underlying + persistent volume is being resized. + type: string + status: + type: string + type: + description: PersistentVolumeClaimConditionType + is a valid value of PersistentVolumeClaimCondition.Type + type: string + required: + - status + - type + type: object + type: array + currentVolumeAttributesClassName: + description: |- + currentVolumeAttributesClassName is the current name of the VolumeAttributesClass the PVC is using. + When unset, there is no VolumeAttributeClass applied to this PersistentVolumeClaim + This is an alpha field and requires enabling VolumeAttributesClass feature. + type: string + modifyVolumeStatus: + description: |- + ModifyVolumeStatus represents the status object of ControllerModifyVolume operation. + When this is unset, there is no ModifyVolume operation being attempted. + This is an alpha field and requires enabling VolumeAttributesClass feature. + properties: + status: + description: "status is the status of the ControllerModifyVolume + operation. It can be in any of following states:\n + - Pending\n Pending indicates that the PersistentVolumeClaim + cannot be modified due to unmet requirements, such + as\n the specified VolumeAttributesClass not existing.\n + - InProgress\n InProgress indicates that the volume + is being modified.\n - Infeasible\n Infeasible + indicates that the request has been rejected as + invalid by the CSI driver. To\n\t resolve the error, + a valid VolumeAttributesClass needs to be specified.\nNote: + New statuses can be added in the future. Consumers + should check for unknown statuses and fail appropriately." + type: string + targetVolumeAttributesClassName: + description: targetVolumeAttributesClassName is the + name of the VolumeAttributesClass the PVC currently + being reconciled + type: string + required: + - status + type: object + phase: + description: phase represents the current phase of PersistentVolumeClaim. + type: string + type: object + type: object + volumeMount: + description: Additional Volume is provided by user that is mounted + on the pods + properties: + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + volume: + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk + mount on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk + in the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in + the blob storage + type: string + fsType: + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage account Managed: azure + managed data disk (only in managed availability + set). defaults to shared' + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the + host that shares a pod's lifetime + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + items: + type: string + type: array + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default + is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: boolean + secretFile: + description: |- + secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + secretRef: + description: |- + secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: |- + defaultMode is optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers (Beta feature). + properties: + driver: + description: |- + driver is the name of the CSI driver that handles this volume. + Consult with your admin for the correct name as registered in the cluster. + type: string + fsType: + description: |- + fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated CSI driver + which will determine the default filesystem to apply. + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no secret is required. If the + secret object contains more than one secret, all secret references are passed. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. Consult your driver's documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about + the pod that should populate this volume + properties: + defaultMode: + description: |- + Optional: mode bits to use on created files by default. Must be a + Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name + and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: |- + ephemeral represents a volume that is handled by a cluster storage driver. + The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, + and deleted when the pod is removed. + + Use this if: + a) the volume is only needed while the pod runs, + b) features of normal volumes like restoring from snapshot or capacity + tracking are needed, + c) the storage driver is specified through a storage class, and + d) the storage driver supports dynamic volume provisioning through + a PersistentVolumeClaim (see EphemeralVolumeSource for more + information on the connection between this volume type + and PersistentVolumeClaim). + + Use PersistentVolumeClaim or one of the vendor-specific + APIs for volumes that persist for longer than the lifecycle + of an individual pod. + + Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to + be used that way - see the documentation of the driver for + more information. + + A pod can use both types of ephemeral volumes and + persistent volumes at the same time. + properties: + volumeClaimTemplate: + description: |- + Will be used to create a stand-alone PVC to provision the volume. + The pod in which this EphemeralVolumeSource is embedded will be the + owner of the PVC, i.e. the PVC will be deleted together with the + pod. The name of the PVC will be `<pod name>-<volume name>` where + `<volume name>` is the name from the `PodSpec.Volumes` array + entry. Pod validation will reject the pod if the concatenated name + is not valid for a PVC (for example, too long). + + An existing PVC with that name that is not owned by the pod + will *not* be used for the pod to avoid using an unrelated + volume by mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created PVC is + meant to be used by the pod, the PVC has to updated with an + owner reference to the pod once the pod exists. Normally + this should not be necessary, but it may be useful when + manually reconstructing a broken cluster. + + This field is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. + + Required, must not be nil. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. No other fields are allowed and will be rejected during + validation. + type: object + spec: + description: |- + The specification for the PersistentVolumeClaim. The entire content is + copied unchanged into the PVC that gets created from this + template. The same fields as in a PersistentVolumeClaim + are also valid here. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource + that is attached to a kubelet's host machine and then + exposed to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target + worldwide names (WWNs)' + items: + type: string + type: array + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. + items: + type: string + type: array + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to + use for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the plugin scripts. This may be + empty if no secret object is specified. If the secret object + contains more than one secret, all secrets are passed to the plugin + scripts. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. This depends on the Flocker + control service being running + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + properties: + fsType: + description: |- + fsType is filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an + EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir + into the Pod's container. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '.' is supplied, the volume directory will be the + git repository. Otherwise, if specified, the volume will contain the git repository in + the subdirectory with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the + specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: |- + glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/glusterfs/README.md + properties: + endpoints: + description: |- + endpoints is the endpoint name that details Glusterfs topology. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. This is generally + used for system agents or other privileged things that are allowed + to see the host machine. Most containers will NOT need this. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://examples.k8s.io/volumes/iscsi/README.md + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi + type: string + initiatorName: + description: |- + initiatorName is the custom iSCSI Initiator Name. + If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + <target portal>:<volume name> will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: |- + portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: |- + targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon + Controller persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources + secrets, configmaps, and downward API + properties: + defaultMode: + description: |- + defaultMode are the mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: sources is the list of volume projections + items: + description: Projection that may be projected + along with other supported volume types + properties: + clusterTrustBundle: + description: |- + ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field + of ClusterTrustBundle objects in an auto-updating file. + + Alpha, gated by the ClusterTrustBundleProjection feature gate. + + ClusterTrustBundle objects can either be selected by name, or by the + combination of signer name and a label selector. + + Kubelet performs aggressive normalization of the PEM contents written + into the pod filesystem. Esoteric PEM features such as inter-block + comments and block headers are stripped. Certificates are deduplicated. + The ordering of certificates within the file is arbitrary, and Kubelet + may change the order over time. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. Mutually-exclusive with name. If unset, + interpreted as "match nothing". If set but empty, interpreted as "match + everything". + properties: + matchExpressions: + description: matchExpressions is a + list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. If using name, then the named ClusterTrustBundle is + allowed not to exist. If using signerName, then the combination of + signerName and labelSelector is allowed to match zero + ClusterTrustBundles. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. The contents of all selected + ClusterTrustBundles will be unified and deduplicated. + type: string + required: + - path + type: object + configMap: + description: configMap information about the + configMap data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether + the ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about + the downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects + a field of the pod: only annotations, + labels, name and namespace are + supported.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file + to be created. Must not be absolute + or contain the ''..'' path. Must + be utf-8 encoded. The first item + of the relative path must not + start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + secret: + description: secret information about the + secret data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: |- + audience is the intended audience of the token. A recipient of a token + must identify itself with an identifier specified in the audience of the + token, and otherwise should reject the token. The audience defaults to the + identifier of the apiserver. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. As the token approaches expiration, the kubelet volume + plugin will proactively rotate the service account token. The kubelet will + start trying to rotate the token if the token is older than 80 percent of + its time to live or if the token is older than 24 hours.Defaults to 1 hour + and must be at least 10 minutes. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + description: quobyte represents a Quobyte mount on the + host that shares a pod's lifetime + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple entries are separated with commas) + which acts as the central registry for volumes + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set by the plugin + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references + an already created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: |- + rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/rbd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + pool: + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent + volume attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". + Default is "xfs". + type: string + gateway: + description: gateway is the host address of the + ScaleIO API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the + ScaleIO Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. If this is not provided, Login operation will fail. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL + communication with Gateway, default false + type: boolean + storageMode: + description: |- + storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage + Pool associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the + Secret or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. If not specified, default values will be attempted. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: |- + volumeNamespace specifies the scope of the volume within StorageOS. If no + namespace is specified then the Pod's namespace will be used. This allows the + Kubernetes name scoping to be mirrored within StorageOS for tighter integration. + Set VolumeName to any name to override the default behaviour. + Set to "default" if you are not using namespaces within StorageOS. + Namespaces that do not pre-exist within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy + Based Management (SPBM) profile ID associated + with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + type: object + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple <key,value,effect> using the matching operator <operator>. + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + items: + description: TopologySpreadConstraint specifies how to spread matching + pods among the given topology. + properties: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 + type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. + + This is a beta field and requires the MinDomainsInPodTopologySpread feature gate to be enabled (enabled by default). + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + If this value is nil, the behavior is equivalent to the Honor policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + If this value is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each <key, value> as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + required: + - clusterSize + - kubernetesConfig + type: object + status: + description: RedisStatus defines the observed state of Redis + properties: + masterNode: + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redissentinels.yaml b/pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redissentinels.yaml new file mode 100644 index 00000000..b87a1753 --- /dev/null +++ b/pkg/vendored/redis-operator/crds/redis.redis.opstreelabs.in_redissentinels.yaml @@ -0,0 +1,5201 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: redissentinels.redis.redis.opstreelabs.in +spec: + group: redis.redis.opstreelabs.in + names: + kind: RedisSentinel + listKind: RedisSentinelList + plural: redissentinels + singular: redissentinel + scope: Namespaced + versions: + - name: v1beta2 + schema: + openAPIV3Schema: + description: Redis is the Schema for the redis API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + TLS: + description: TLS Configuration for redis instances + properties: + ca: + type: string + cert: + type: string + key: + type: string + secret: + description: Reference to secret which contains the certificates + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret or + its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + required: + - secret + type: object + affinity: + description: Affinity is a group of affinity scheduling rules. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the + pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key <topologyKey> matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + clusterSize: + default: 3 + format: int32 + minimum: 1 + type: integer + env: + items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. Cannot + be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + hostPort: + type: integer + initContainer: + description: InitContainer for each Redis pods + properties: + args: + items: + type: string + type: array + command: + items: + type: string + type: array + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + kubernetesConfig: + description: KubernetesConfig will be the JSON struct for Basic Redis + Config + properties: + ignoreAnnotations: + items: + type: string + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + imagePullSecrets: + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + minReadySeconds: + format: int32 + type: integer + persistentVolumeClaimRetentionPolicy: + description: |- + StatefulSetPersistentVolumeClaimRetentionPolicy describes the policy used for PVCs + created from the StatefulSet VolumeClaimTemplates. + properties: + whenDeleted: + description: |- + WhenDeleted specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is deleted. The default policy + of `Retain` causes PVCs to not be affected by StatefulSet deletion. The + `Delete` policy causes those PVCs to be deleted. + type: string + whenScaled: + description: |- + WhenScaled specifies what happens to PVCs created from StatefulSet + VolumeClaimTemplates when the StatefulSet is scaled down. The default + policy of `Retain` causes PVCs to not be affected by a scaledown. The + `Delete` policy causes the associated PVCs for any excess pods above + the replica count to be deleted. + type: string + type: object + redisSecret: + description: ExistingPasswordSecret is the struct to access the + existing secret + properties: + key: + type: string + name: + type: string + type: object + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + service: + description: ServiceConfig define the type of service to be created + and its annotations + properties: + additional: + description: Additional config for which suffix is -additional + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + annotations: + additionalProperties: + type: string + type: object + headless: + description: Headless config for which suffix is -headless + service + properties: + additionalAnnotations: + additionalProperties: + type: string + type: object + enabled: + default: true + type: boolean + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + type: + default: ClusterIP + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + includeBusPort: + description: |- + IncludeBusPort when set to true, it will add bus port to the service, such as 16379. + This field is only used for Redis cluster mode. + type: boolean + serviceType: + enum: + - LoadBalancer + - NodePort + - ClusterIP + type: string + type: object + updateStrategy: + description: |- + StatefulSetUpdateStrategy indicates the strategy that the StatefulSet + controller will use to perform updates. It includes any additional parameters + necessary to perform the update for the indicated strategy. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be unavailable during the update. + Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + Absolute number is calculated from percentage by rounding up. This can not be 0. + Defaults to 1. This field is alpha-level and is only honored by servers that enable the + MaxUnavailableStatefulSet feature. The field applies to all pods in the range 0 to + Replicas-1. That means if there is any unavailable pod in the range 0 to Replicas-1, it + will be counted towards MaxUnavailable. + x-kubernetes-int-or-string: true + partition: + description: |- + Partition indicates the ordinal at which the StatefulSet should be partitioned + for updates. During a rolling update, all pods from ordinal Replicas-1 to + Partition are updated. All pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: |- + Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object + required: + - image + type: object + livenessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + nodeSelector: + additionalProperties: + type: string + type: object + pdb: + description: RedisPodDisruptionBudget configure a PodDisruptionBudget + on the resource (leader/follower) + properties: + enabled: + type: boolean + maxUnavailable: + format: int32 + type: integer + minAvailable: + format: int32 + type: integer + type: object + podSecurityContext: + description: |- + PodSecurityContext holds pod-level security attributes and common container settings. + Some fields are also present in container.securityContext. Field values of + container.securityContext take precedence over field values of PodSecurityContext. + properties: + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + priorityClassName: + type: string + readinessProbe: + description: |- + Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: |- + Command is the command line to execute inside the container, the working directory for the + command is root ('/') in the container's filesystem. The command is simply exec'd, it is + not run inside a shell, so traditional shell instructions ('|', etc) won't work. To use + a shell, you need to explicitly call out to that shell. + Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: |- + Minimum consecutive failures for the probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving a GRPC port. + properties: + port: + description: Port number of the gRPC service. Number must + be in the range 1 to 65535. + format: int32 + type: integer + service: + default: "" + description: |- + Service is the name of the service to place in the gRPC HealthCheckRequest + (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + + If this is not specified, the default behavior is defined by gRPC. + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request to perform. + properties: + host: + description: |- + Host name to connect to, defaults to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. HTTP allows + repeated headers. + items: + description: HTTPHeader describes a custom header to be + used in HTTP probes + properties: + name: + description: |- + The header field name. + This will be canonicalized upon output, so case-variant names will be understood as the same header. + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Name or number of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: |- + Scheme to use for connecting to the host. + Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: |- + Number of seconds after the container has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + periodSeconds: + description: |- + How often (in seconds) to perform the probe. + Default to 10 seconds. Minimum value is 1. + format: int32 + type: integer + successThreshold: + description: |- + Minimum consecutive successes for the probe to be considered successful after having failed. + Defaults to 1. Must be 1 for liveness and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving a TCP port. + properties: + host: + description: 'Optional: Host name to connect to, defaults + to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the container. + Number must be in the range 1 to 65535. + Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: |- + Optional duration in seconds the pod needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after the processes running in the pod are sent + a termination signal and the time when the processes are forcibly halted with a kill signal. + Set this value longer than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this + value overrides the value provided by the pod spec. + Value must be non-negative integer. The value zero indicates stop immediately via + the kill signal (no opportunity to shut down). + This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: |- + Number of seconds after which the probe times out. + Defaults to 1 second. Minimum value is 1. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + format: int32 + type: integer + type: object + redisExporter: + description: RedisExporter interface will have the information for + redis exporter related stuff + properties: + enabled: + type: boolean + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + port: + default: 9121 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + type: object + redisSentinelConfig: + properties: + additionalSentinelConfig: + type: string + announceHostnames: + default: "no" + type: string + downAfterMilliseconds: + default: "30000" + type: string + failoverTimeout: + default: "180000" + type: string + masterGroupName: + default: myMaster + type: string + parallelSyncs: + default: "1" + type: string + quorum: + default: "2" + type: string + redisPort: + default: "6379" + type: string + redisReplicationName: + type: string + redisReplicationPassword: + description: EnvVarSource represents a source for the value of + an EnvVar. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + resolveHostnames: + default: "no" + type: string + required: + - redisReplicationName + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies to + the container. + type: string + role: + description: Role is a SELinux role label that applies to + the container. + type: string + type: + description: Type is a SELinux type label that applies to + the container. + type: string + user: + description: User is a SELinux user label that applies to + the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the GMSA + credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + serviceAccountName: + type: string + sidecars: + items: + description: Sidecar for each Redis pods + properties: + command: + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be + a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to pull + a container image + type: string + mountPath: + items: + description: VolumeMount describes a mounting of a Volume + within a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + name: + type: string + ports: + items: + description: ContainerPort represents a network port in a + single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext holds security configuration that will be applied to a container. + Some fields are present in both SecurityContext and PodSecurityContext. When both + are set, the values in SecurityContext take precedence. + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default is DefaultProcMount which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + required: + - image + - name + type: object + type: array + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple <key,value,effect> using the matching operator <operator>. + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + items: + description: TopologySpreadConstraint specifies how to spread matching + pods among the given topology. + properties: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 + type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. + + This is a beta field and requires the MinDomainsInPodTopologySpread feature gate to be enabled (enabled by default). + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + If this value is nil, the behavior is equivalent to the Honor policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + If this value is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each <key, value> as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + volumeMount: + description: Additional Volume is provided by user that is mounted + on the pods + properties: + mountPath: + items: + description: VolumeMount describes a mounting of a Volume within + a container. + properties: + mountPath: + description: |- + Path within the container at which the volume should be mounted. Must + not contain ':'. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + volume: + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: |- + awsElasticBlockStore represents an AWS Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + format: int32 + type: integer + readOnly: + description: |- + readOnly value true will force the readOnly setting in VolumeMounts. + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: boolean + volumeID: + description: |- + volumeID is unique ID of the persistent disk resource in AWS (Amazon EBS volume). + More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk mount + on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk in + the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in the + blob storage + type: string + fsType: + description: |- + fsType is Filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage account Managed: azure managed + data disk (only in managed availability set). defaults + to shared' + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the host + that shares a pod's lifetime + properties: + monitors: + description: |- + monitors is Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + items: + type: string + type: array + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default is /' + type: string + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: boolean + secretFile: + description: |- + secretFile is Optional: SecretFile is the path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + secretRef: + description: |- + secretRef is Optional: SecretRef is reference to the authentication secret for User, default is empty. + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is optional: User is the rados user name, default is admin + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it + type: string + required: + - monitors + type: object + cinder: + description: |- + cinder represents a cinder volume attached and mounted on kubelets host machine. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: boolean + secretRef: + description: |- + secretRef is optional: points to a secret object containing parameters used to connect + to OpenStack. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + description: |- + volumeID used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: |- + defaultMode is optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers (Beta feature). + properties: + driver: + description: |- + driver is the name of the CSI driver that handles this volume. + Consult with your admin for the correct name as registered in the cluster. + type: string + fsType: + description: |- + fsType to mount. Ex. "ext4", "xfs", "ntfs". + If not provided, the empty value is passed to the associated CSI driver + which will determine the default filesystem to apply. + type: string + nodePublishSecretRef: + description: |- + nodePublishSecretRef is a reference to the secret object containing + sensitive information to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no secret is required. If the + secret object contains more than one secret, all secret references are passed. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + description: |- + readOnly specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: |- + volumeAttributes stores driver-specific properties that are passed to the CSI + driver. Consult your driver's documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about the + pod that should populate this volume + properties: + defaultMode: + description: |- + Optional: mode bits to use on created files by default. Must be a + Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field + properties: + fieldRef: + description: 'Required: Selects a field of the + pod: only annotations, labels, name and namespace + are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must not + be absolute or contain the ''..'' path. Must + be utf-8 encoded. The first item of the relative + path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + emptyDir: + description: |- + emptyDir represents a temporary directory that shares a pod's lifetime. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: |- + ephemeral represents a volume that is handled by a cluster storage driver. + The volume's lifecycle is tied to the pod that defines it - it will be created before the pod starts, + and deleted when the pod is removed. + + Use this if: + a) the volume is only needed while the pod runs, + b) features of normal volumes like restoring from snapshot or capacity + tracking are needed, + c) the storage driver is specified through a storage class, and + d) the storage driver supports dynamic volume provisioning through + a PersistentVolumeClaim (see EphemeralVolumeSource for more + information on the connection between this volume type + and PersistentVolumeClaim). + + Use PersistentVolumeClaim or one of the vendor-specific + APIs for volumes that persist for longer than the lifecycle + of an individual pod. + + Use CSI for light-weight local ephemeral volumes if the CSI driver is meant to + be used that way - see the documentation of the driver for + more information. + + A pod can use both types of ephemeral volumes and + persistent volumes at the same time. + properties: + volumeClaimTemplate: + description: |- + Will be used to create a stand-alone PVC to provision the volume. + The pod in which this EphemeralVolumeSource is embedded will be the + owner of the PVC, i.e. the PVC will be deleted together with the + pod. The name of the PVC will be `<pod name>-<volume name>` where + `<volume name>` is the name from the `PodSpec.Volumes` array + entry. Pod validation will reject the pod if the concatenated name + is not valid for a PVC (for example, too long). + + An existing PVC with that name that is not owned by the pod + will *not* be used for the pod to avoid using an unrelated + volume by mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created PVC is + meant to be used by the pod, the PVC has to updated with an + owner reference to the pod once the pod exists. Normally + this should not be necessary, but it may be useful when + manually reconstructing a broken cluster. + + This field is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. + + Required, must not be nil. + properties: + metadata: + description: |- + May contain labels and annotations that will be copied into the PVC + when creating it. No other fields are allowed and will be rejected during + validation. + type: object + spec: + description: |- + The specification for the PersistentVolumeClaim. The entire content is + copied unchanged into the PVC that gets created from this + template. The same fields as in a PersistentVolumeClaim + are also valid here. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string value means that no VolumeAttributesClass + will be applied to the claim but it's not allowed to reset this field to empty string once it is set. + If unspecified and the PersistentVolumeClaim is unbound, the default VolumeAttributesClass + will be set by the persistentvolume controller if it exists. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#volumeattributesclass + (Alpha) Using this field requires the VolumeAttributesClass feature gate to be enabled. + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource that + is attached to a kubelet's host machine and then exposed + to the pod. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: |- + readOnly is Optional: Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target worldwide + names (WWNs)' + items: + type: string + type: array + wwids: + description: |- + wwids Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be set, but not both simultaneously. + items: + type: string + type: array + type: object + flexVolume: + description: |- + flexVolume represents a generic volume resource that is + provisioned/attached using an exec based plugin. + properties: + driver: + description: driver is the name of the driver to use + for this volume. + type: string + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". The default filesystem depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: |- + readOnly is Optional: defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef is Optional: secretRef is reference to the secret object containing + sensitive information to pass to the plugin scripts. This may be + empty if no secret object is specified. If the secret object + contains more than one secret, all secrets are passed to the plugin + scripts. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. This depends on the Flocker + control service being running + properties: + datasetName: + description: |- + datasetName is Name of the dataset stored as metadata -> name on the dataset for Flocker + should be considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: |- + gcePersistentDisk represents a GCE Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + properties: + fsType: + description: |- + fsType is filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + partition: + description: |- + partition is the partition in the volume that you want to mount. + If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition as "1". + Similarly, the volume partition for /dev/sda is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + format: int32 + type: integer + pdName: + description: |- + pdName is unique name of the PD resource in GCE. Used to identify the disk in GCE. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + type: boolean + required: + - pdName + type: object + gitRepo: + description: |- + gitRepo represents a git repository at a particular revision. + DEPRECATED: GitRepo is deprecated. To provision a container with a git repo, mount an + EmptyDir into an InitContainer that clones the repo using git, then mount the EmptyDir + into the Pod's container. + properties: + directory: + description: |- + directory is the target directory name. + Must not contain or start with '..'. If '.' is supplied, the volume directory will be the + git repository. Otherwise, if specified, the volume will contain the git repository in + the subdirectory with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the specified + revision. + type: string + required: + - repository + type: object + glusterfs: + description: |- + glusterfs represents a Glusterfs mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/glusterfs/README.md + properties: + endpoints: + description: |- + endpoints is the endpoint name that details Glusterfs topology. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + path: + description: |- + path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: string + readOnly: + description: |- + readOnly here will force the Glusterfs volume to be mounted with read-only permissions. + Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: |- + hostPath represents a pre-existing file or directory on the host + machine that is directly exposed to the container. This is generally + used for system agents or other privileged things that are allowed + to see the host machine. Most containers will NOT need this. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + properties: + path: + description: |- + path of the directory on the host. + If the path is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + type: + description: |- + type for HostPath Volume + Defaults to "" + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + type: string + required: + - path + type: object + iscsi: + description: |- + iscsi represents an ISCSI Disk resource that is attached to a + kubelet's host machine and then exposed to the pod. + More info: https://examples.k8s.io/volumes/iscsi/README.md + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi + type: string + initiatorName: + description: |- + initiatorName is the custom iSCSI Initiator Name. + If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + <target portal>:<volume name> will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + description: |- + iscsiInterface is the interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: |- + portals is the iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + description: |- + targetPortal is iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port + is other than default (typically TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: |- + name of the volume. + Must be a DNS_LABEL and unique within the pod. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + nfs: + description: |- + nfs represents an NFS mount on the host that shares a pod's lifetime + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + properties: + path: + description: |- + path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + readOnly: + description: |- + readOnly here will force the NFS export to be mounted with read-only permissions. + Defaults to false. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: boolean + server: + description: |- + server is the hostname or IP address of the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: |- + persistentVolumeClaimVolumeSource represents a reference to a + PersistentVolumeClaim in the same namespace. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + properties: + claimName: + description: |- + claimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + type: string + readOnly: + description: |- + readOnly Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon Controller + persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine + properties: + fsType: + description: |- + fSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources secrets, + configmaps, and downward API + properties: + defaultMode: + description: |- + defaultMode are the mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: sources is the list of volume projections + items: + description: Projection that may be projected along + with other supported volume types + properties: + clusterTrustBundle: + description: |- + ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field + of ClusterTrustBundle objects in an auto-updating file. + + Alpha, gated by the ClusterTrustBundleProjection feature gate. + + ClusterTrustBundle objects can either be selected by name, or by the + combination of signer name and a label selector. + + Kubelet performs aggressive normalization of the PEM contents written + into the pod filesystem. Esoteric PEM features such as inter-block + comments and block headers are stripped. Certificates are deduplicated. + The ordering of certificates within the file is arbitrary, and Kubelet + may change the order over time. + properties: + labelSelector: + description: |- + Select all ClusterTrustBundles that match this label selector. Only has + effect if signerName is set. Mutually-exclusive with name. If unset, + interpreted as "match nothing". If set but empty, interpreted as "match + everything". + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Select a single ClusterTrustBundle by object name. Mutually-exclusive + with signerName and labelSelector. + type: string + optional: + description: |- + If true, don't block pod startup if the referenced ClusterTrustBundle(s) + aren't available. If using name, then the named ClusterTrustBundle is + allowed not to exist. If using signerName, then the combination of + signerName and labelSelector is allowed to match zero + ClusterTrustBundles. + type: boolean + path: + description: Relative path from the volume + root to write the bundle. + type: string + signerName: + description: |- + Select all ClusterTrustBundles that match this signer name. + Mutually-exclusive with name. The contents of all selected + ClusterTrustBundles will be unified and deduplicated. + type: string + required: + - path + type: object + configMap: + description: configMap information about the configMap + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + ConfigMap will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the ConfigMap, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional specify whether the + ConfigMap or its keys must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + description: downwardAPI information about the + downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects a field + of the pod: only annotations, labels, + name and namespace are supported.' + properties: + apiVersion: + description: Version of the schema + the FieldPath is written in terms + of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to + select in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + description: |- + Optional: mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file to + be created. Must not be absolute or + contain the ''..'' path. Must be utf-8 + encoded. The first item of the relative + path must not start with ''..''' + type: string + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. + properties: + containerName: + description: 'Container name: required + for volumes, optional for env + vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + type: object + secret: + description: secret information about the secret + data to project + properties: + items: + description: |- + items if unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path + within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: |- + audience is the intended audience of the token. A recipient of a token + must identify itself with an identifier specified in the audience of the + token, and otherwise should reject the token. The audience defaults to the + identifier of the apiserver. + type: string + expirationSeconds: + description: |- + expirationSeconds is the requested duration of validity of the service + account token. As the token approaches expiration, the kubelet volume + plugin will proactively rotate the service account token. The kubelet will + start trying to rotate the token if the token is older than 80 percent of + its time to live or if the token is older than 24 hours.Defaults to 1 hour + and must be at least 10 minutes. + format: int64 + type: integer + path: + description: |- + path is the path relative to the mount point of the file to project the + token into. + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + description: quobyte represents a Quobyte mount on the host + that shares a pod's lifetime + properties: + group: + description: |- + group to map volume access to + Default is no group + type: string + readOnly: + description: |- + readOnly here will force the Quobyte volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: |- + registry represents a single or multiple Quobyte Registry services + specified as a string as host:port pair (multiple entries are separated with commas) + which acts as the central registry for volumes + type: string + tenant: + description: |- + tenant owning the given Quobyte volume in the Backend + Used with dynamically provisioned Quobyte volumes, value is set by the plugin + type: string + user: + description: |- + user to map volume access to + Defaults to serivceaccount user + type: string + volume: + description: volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: |- + rbd represents a Rados Block Device mount on the host that shares a pod's lifetime. + More info: https://examples.k8s.io/volumes/rbd/README.md + properties: + fsType: + description: |- + fsType is the filesystem type of the volume that you want to mount. + Tip: Ensure that the filesystem type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd + type: string + image: + description: |- + image is the rados image name. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + keyring: + description: |- + keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + monitors: + description: |- + monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + items: + type: string + type: array + pool: + description: |- + pool is the rados pool name. + Default is rbd. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + readOnly: + description: |- + readOnly here will force the ReadOnly setting in VolumeMounts. + Defaults to false. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: boolean + secretRef: + description: |- + secretRef is name of the authentication secret for RBDUser. If provided + overrides keyring. + Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + user: + description: |- + user is the rados user name. + Default is admin. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". + Default is "xfs". + type: string + gateway: + description: gateway is the host address of the ScaleIO + API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the ScaleIO + Protection Domain for the configured storage. + type: string + readOnly: + description: |- + readOnly Defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef references to the secret for ScaleIO user and other + sensitive information. If this is not provided, Login operation will fail. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + description: sslEnabled Flag enable/disable SSL communication + with Gateway, default false + type: boolean + storageMode: + description: |- + storageMode indicates whether the storage for a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage Pool + associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: |- + volumeName is the name of a volume already created in the ScaleIO system + that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: |- + secret represents a secret that should populate this volume. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + properties: + defaultMode: + description: |- + defaultMode is Optional: mode bits used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values + for mode bits. Defaults to 0644. + Directories within the path are not affected by this setting. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + items: + description: |- + items If unspecified, each key-value pair in the Data field of the referenced + Secret will be projected into the volume as a file whose name is the + key and content is the value. If specified, the listed keys will be + projected into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. Paths must be + relative and may not contain the '..' path or start with '..'. + items: + description: Maps a string key to a path within a + volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: |- + mode is Optional: mode bits used to set permissions on this file. + Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + If not specified, the volume defaultMode will be used. + This might be in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + path: + description: |- + path is the relative path of the file to map the key to. + May not be an absolute path. + May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the Secret + or its keys must be defined + type: boolean + secretName: + description: |- + secretName is the name of the secret in the pod's namespace to use. + More info: https://kubernetes.io/docs/concepts/storage/volumes#secret + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: |- + fsType is the filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: |- + readOnly defaults to false (read/write). ReadOnly here will force + the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: |- + secretRef specifies the secret to use for obtaining the StorageOS API + credentials. If not specified, default values will be attempted. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: |- + volumeNamespace specifies the scope of the volume within StorageOS. If no + namespace is specified then the Pod's namespace will be used. This allows the + Kubernetes name scoping to be mirrored within StorageOS for tighter integration. + Set VolumeName to any name to override the default behaviour. + Set to "default" if you are not using namespaces within StorageOS. + Namespaces that do not pre-exist within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy Based + Management (SPBM) profile ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + required: + - clusterSize + - kubernetesConfig + type: object + status: + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/vendored/redis-operator/redis/v1beta2/README.md b/pkg/vendored/redis-operator/redis/v1beta2/README.md new file mode 100644 index 00000000..41280f93 --- /dev/null +++ b/pkg/vendored/redis-operator/redis/v1beta2/README.md @@ -0,0 +1,30 @@ +# Redis API Types (v1beta2) + +## Source +- **Package**: `github.com/OT-CONTAINER-KIT/redis-operator/api/redis/v1beta2` +- **Version**: v0.22.1 +- **Commit**: Corresponds to v0.22.1 release tag +- **Cloned From**: `/Users/j7m4/go/pkg/mod/github.com/!o!t-!c!o!n!t!a!i!n!e!r-!k!i!t/redis-operator@v0.22.1/api/redis/v1beta2/` + +## Contents +CRD types for standalone Redis (single instance): +- `Redis` - Main CRD type +- `RedisSpec` - Specification structure +- `RedisStatus` - Status structure +- Webhook setup (no compilation issues) + +## Modifications + +### redis_types.go +**Line 20**: Updated import path to use vendored common package +```go +common "github.com/wandb/operator/api/redis-operator/model/v1beta2" +``` + +### zz_generated.deepcopy.go +**Line 24**: Updated import path to use vendored common package +```go +commonv1beta2 "github.com/wandb/operator/api/redis-operator/model/v1beta2" +``` + +**Reason**: This package doesn't have the webhook.Validator bug, but it needs to be vendored for type consistency since it uses the `common/v1beta2` types which are also vendored. diff --git a/pkg/vendored/redis-operator/redis/v1beta2/groupversion_info.go b/pkg/vendored/redis-operator/redis/v1beta2/groupversion_info.go new file mode 100644 index 00000000..65673400 --- /dev/null +++ b/pkg/vendored/redis-operator/redis/v1beta2/groupversion_info.go @@ -0,0 +1,34 @@ +/* +Copyright 2020 Opstree Solutions. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta2 contains API Schema definitions for the redis v1beta2 API group +package v1beta2 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "redis.redis.opstreelabs.in", Version: "v1beta2"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/pkg/vendored/redis-operator/redis/v1beta2/redis_conversion.go b/pkg/vendored/redis-operator/redis/v1beta2/redis_conversion.go new file mode 100644 index 00000000..8e113e71 --- /dev/null +++ b/pkg/vendored/redis-operator/redis/v1beta2/redis_conversion.go @@ -0,0 +1,4 @@ +package v1beta2 + +// Hub marks this type as a conversion hub. +func (*Redis) Hub() {} diff --git a/pkg/vendored/redis-operator/redis/v1beta2/redis_types.go b/pkg/vendored/redis-operator/redis/v1beta2/redis_types.go new file mode 100644 index 00000000..087a6081 --- /dev/null +++ b/pkg/vendored/redis-operator/redis/v1beta2/redis_types.go @@ -0,0 +1,74 @@ +/* +Copyright 2020 Opstree Solutions. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +import ( + common "github.com/wandb/operator/pkg/vendored/redis-operator/common/v1beta2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// RedisSpec defines the desired state of Redis +type RedisSpec struct { + KubernetesConfig common.KubernetesConfig `json:"kubernetesConfig"` + RedisExporter *common.RedisExporter `json:"redisExporter,omitempty"` + RedisConfig *common.RedisConfig `json:"redisConfig,omitempty"` + Storage *common.Storage `json:"storage,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + PodSecurityContext *corev1.PodSecurityContext `json:"podSecurityContext,omitempty"` + SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"` + PriorityClassName string `json:"priorityClassName,omitempty"` + Affinity *corev1.Affinity `json:"affinity,omitempty"` + Tolerations *[]corev1.Toleration `json:"tolerations,omitempty"` + TLS *common.TLSConfig `json:"TLS,omitempty"` + ACL *common.ACLConfig `json:"acl,omitempty"` + ReadinessProbe *corev1.Probe `json:"readinessProbe,omitempty" protobuf:"bytes,11,opt,name=readinessProbe"` + LivenessProbe *corev1.Probe `json:"livenessProbe,omitempty" protobuf:"bytes,12,opt,name=livenessProbe"` + InitContainer *common.InitContainer `json:"initContainer,omitempty"` + Sidecars *[]common.Sidecar `json:"sidecars,omitempty"` + ServiceAccountName *string `json:"serviceAccountName,omitempty"` + TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty" protobuf:"varint,4,opt,name=terminationGracePeriodSeconds"` + EnvVars *[]corev1.EnvVar `json:"env,omitempty"` + HostPort *int `json:"hostPort,omitempty"` +} + +// RedisStatus defines the observed state of Redis +type RedisStatus struct{} + +// Redis is the Schema for the redis API +type Redis struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RedisSpec `json:"spec"` + Status RedisStatus `json:"status,omitempty"` +} + +// RedisList contains a list of Redis +type RedisList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Redis `json:"items"` +} + +//nolint:gochecknoinits +func init() { + SchemeBuilder.Register(&Redis{}, &RedisList{}) +} diff --git a/pkg/vendored/redis-operator/redis/v1beta2/redis_webhook.go b/pkg/vendored/redis-operator/redis/v1beta2/redis_webhook.go new file mode 100644 index 00000000..c00eae82 --- /dev/null +++ b/pkg/vendored/redis-operator/redis/v1beta2/redis_webhook.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 Opstree Solutions. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +import ( + ctrl "sigs.k8s.io/controller-runtime" +) + +func (r *Redis) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! diff --git a/pkg/vendored/redis-operator/redis/v1beta2/zz_generated.deepcopy.go b/pkg/vendored/redis-operator/redis/v1beta2/zz_generated.deepcopy.go new file mode 100644 index 00000000..afc35bbc --- /dev/null +++ b/pkg/vendored/redis-operator/redis/v1beta2/zz_generated.deepcopy.go @@ -0,0 +1,227 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta2 + +import ( + commonv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/common/v1beta2" + "k8s.io/api/core/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Redis) DeepCopyInto(out *Redis) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Redis. +func (in *Redis) DeepCopy() *Redis { + if in == nil { + return nil + } + out := new(Redis) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Redis) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisList) DeepCopyInto(out *RedisList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Redis, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisList. +func (in *RedisList) DeepCopy() *RedisList { + if in == nil { + return nil + } + out := new(RedisList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSpec) DeepCopyInto(out *RedisSpec) { + *out = *in + in.KubernetesConfig.DeepCopyInto(&out.KubernetesConfig) + if in.RedisExporter != nil { + in, out := &in.RedisExporter, &out.RedisExporter + *out = new(commonv1beta2.RedisExporter) + (*in).DeepCopyInto(*out) + } + if in.RedisConfig != nil { + in, out := &in.RedisConfig, &out.RedisConfig + *out = new(commonv1beta2.RedisConfig) + (*in).DeepCopyInto(*out) + } + if in.Storage != nil { + in, out := &in.Storage, &out.Storage + *out = new(commonv1beta2.Storage) + (*in).DeepCopyInto(*out) + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.PodSecurityContext != nil { + in, out := &in.PodSecurityContext, &out.PodSecurityContext + *out = new(v1.PodSecurityContext) + (*in).DeepCopyInto(*out) + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(v1.SecurityContext) + (*in).DeepCopyInto(*out) + } + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(v1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = new([]v1.Toleration) + if **in != nil { + in, out := *in, *out + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(commonv1beta2.TLSConfig) + (*in).DeepCopyInto(*out) + } + if in.ACL != nil { + in, out := &in.ACL, &out.ACL + *out = new(commonv1beta2.ACLConfig) + (*in).DeepCopyInto(*out) + } + if in.ReadinessProbe != nil { + in, out := &in.ReadinessProbe, &out.ReadinessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.LivenessProbe != nil { + in, out := &in.LivenessProbe, &out.LivenessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.InitContainer != nil { + in, out := &in.InitContainer, &out.InitContainer + *out = new(commonv1beta2.InitContainer) + (*in).DeepCopyInto(*out) + } + if in.Sidecars != nil { + in, out := &in.Sidecars, &out.Sidecars + *out = new([]commonv1beta2.Sidecar) + if **in != nil { + in, out := *in, *out + *out = make([]commonv1beta2.Sidecar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.ServiceAccountName != nil { + in, out := &in.ServiceAccountName, &out.ServiceAccountName + *out = new(string) + **out = **in + } + if in.TerminationGracePeriodSeconds != nil { + in, out := &in.TerminationGracePeriodSeconds, &out.TerminationGracePeriodSeconds + *out = new(int64) + **out = **in + } + if in.EnvVars != nil { + in, out := &in.EnvVars, &out.EnvVars + *out = new([]v1.EnvVar) + if **in != nil { + in, out := *in, *out + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.HostPort != nil { + in, out := &in.HostPort, &out.HostPort + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSpec. +func (in *RedisSpec) DeepCopy() *RedisSpec { + if in == nil { + return nil + } + out := new(RedisSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisStatus) DeepCopyInto(out *RedisStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisStatus. +func (in *RedisStatus) DeepCopy() *RedisStatus { + if in == nil { + return nil + } + out := new(RedisStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/vendored/redis-operator/redisreplication/v1beta2/README.md b/pkg/vendored/redis-operator/redisreplication/v1beta2/README.md new file mode 100644 index 00000000..89f64a1c --- /dev/null +++ b/pkg/vendored/redis-operator/redisreplication/v1beta2/README.md @@ -0,0 +1,22 @@ +# RedisReplication API Types (v1beta2) + +## Source +- **Package**: `github.com/OT-CONTAINER-KIT/redis-operator/api/redisreplication/v1beta2` +- **Version**: v0.22.1 +- **Commit**: Corresponds to v0.22.1 release tag +- **Cloned From**: `/Users/j7m4/go/pkg/mod/github.com/!o!t-!c!o!n!t!a!i!n!e!r-!k!i!t/redis-operator@v0.22.1/api/redisreplication/v1beta2/` + +## Contents +CRD types for Redis replication setup (primary + replicas): +- `RedisReplication` - Main CRD type +- `RedisReplicationSpec` - Specification structure +- `RedisReplicationStatus` - Status structure +- Webhook validation (not used by our operator) + +## Modifications + +### redisreplication_types.go +**Line 4**: Updated import path to use vendored common package +```go +common "github.com/wandb/operator/api/redis-operator/model/v1beta2" +``` diff --git a/pkg/vendored/redis-operator/redisreplication/v1beta2/groupversion_info.go b/pkg/vendored/redis-operator/redisreplication/v1beta2/groupversion_info.go new file mode 100644 index 00000000..65673400 --- /dev/null +++ b/pkg/vendored/redis-operator/redisreplication/v1beta2/groupversion_info.go @@ -0,0 +1,34 @@ +/* +Copyright 2020 Opstree Solutions. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta2 contains API Schema definitions for the redis v1beta2 API group +package v1beta2 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "redis.redis.opstreelabs.in", Version: "v1beta2"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/pkg/vendored/redis-operator/redisreplication/v1beta2/redisreplication_conversion.go b/pkg/vendored/redis-operator/redisreplication/v1beta2/redisreplication_conversion.go new file mode 100644 index 00000000..0d107ca2 --- /dev/null +++ b/pkg/vendored/redis-operator/redisreplication/v1beta2/redisreplication_conversion.go @@ -0,0 +1,4 @@ +package v1beta2 + +// Hub marks this type as a conversion hub. +func (*RedisReplication) Hub() {} diff --git a/pkg/vendored/redis-operator/redisreplication/v1beta2/redisreplication_types.go b/pkg/vendored/redis-operator/redisreplication/v1beta2/redisreplication_types.go new file mode 100644 index 00000000..36dc5557 --- /dev/null +++ b/pkg/vendored/redis-operator/redisreplication/v1beta2/redisreplication_types.go @@ -0,0 +1,68 @@ +package v1beta2 + +import ( + common "github.com/wandb/operator/pkg/vendored/redis-operator/common/v1beta2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type RedisReplicationSpec struct { + Size *int32 `json:"clusterSize"` + KubernetesConfig common.KubernetesConfig `json:"kubernetesConfig"` + RedisExporter *common.RedisExporter `json:"redisExporter,omitempty"` + RedisConfig *common.RedisConfig `json:"redisConfig,omitempty"` + Storage *common.Storage `json:"storage,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + PodSecurityContext *corev1.PodSecurityContext `json:"podSecurityContext,omitempty"` + SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"` + PriorityClassName string `json:"priorityClassName,omitempty"` + Affinity *corev1.Affinity `json:"affinity,omitempty"` + Tolerations *[]corev1.Toleration `json:"tolerations,omitempty"` + TLS *common.TLSConfig `json:"TLS,omitempty"` + PodDisruptionBudget *common.RedisPodDisruptionBudget `json:"pdb,omitempty"` + ACL *common.ACLConfig `json:"acl,omitempty"` + ReadinessProbe *corev1.Probe `json:"readinessProbe,omitempty" protobuf:"bytes,11,opt,name=readinessProbe"` + LivenessProbe *corev1.Probe `json:"livenessProbe,omitempty" protobuf:"bytes,12,opt,name=livenessProbe"` + InitContainer *common.InitContainer `json:"initContainer,omitempty"` + Sidecars *[]common.Sidecar `json:"sidecars,omitempty"` + ServiceAccountName *string `json:"serviceAccountName,omitempty"` + TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty" protobuf:"varint,4,opt,name=terminationGracePeriodSeconds"` + EnvVars *[]corev1.EnvVar `json:"env,omitempty"` + TopologySpreadConstrains []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` + HostPort *int `json:"hostPort,omitempty"` +} + +func (cr *RedisReplicationSpec) GetReplicationCounts(t string) int32 { + replica := cr.Size + return *replica +} + +// RedisStatus defines the observed state of Redis +type RedisReplicationStatus struct { + MasterNode string `json:"masterNode,omitempty"` +} + +// Redis is the Schema for the redis API +type RedisReplication struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RedisReplicationSpec `json:"spec"` + Status RedisReplicationStatus `json:"status,omitempty"` +} + +func (rr *RedisReplication) GetStatefulSetName() string { + return rr.Name +} + +// RedisList contains a list of Redis +type RedisReplicationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RedisReplication `json:"items"` +} + +//nolint:gochecknoinits +func init() { + SchemeBuilder.Register(&RedisReplication{}, &RedisReplicationList{}) +} diff --git a/pkg/vendored/redis-operator/redisreplication/v1beta2/redisreplication_webhook.go b/pkg/vendored/redis-operator/redisreplication/v1beta2/redisreplication_webhook.go new file mode 100644 index 00000000..3474d59c --- /dev/null +++ b/pkg/vendored/redis-operator/redisreplication/v1beta2/redisreplication_webhook.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 Opstree Solutions. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +import ( + ctrl "sigs.k8s.io/controller-runtime" +) + +func (r *RedisReplication) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! diff --git a/pkg/vendored/redis-operator/redisreplication/v1beta2/zz_generated.deepcopy.go b/pkg/vendored/redis-operator/redisreplication/v1beta2/zz_generated.deepcopy.go new file mode 100644 index 00000000..2fcb3738 --- /dev/null +++ b/pkg/vendored/redis-operator/redisreplication/v1beta2/zz_generated.deepcopy.go @@ -0,0 +1,244 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta2 + +import ( + commonv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/common/v1beta2" + "k8s.io/api/core/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisReplication) DeepCopyInto(out *RedisReplication) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisReplication. +func (in *RedisReplication) DeepCopy() *RedisReplication { + if in == nil { + return nil + } + out := new(RedisReplication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisReplication) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisReplicationList) DeepCopyInto(out *RedisReplicationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RedisReplication, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisReplicationList. +func (in *RedisReplicationList) DeepCopy() *RedisReplicationList { + if in == nil { + return nil + } + out := new(RedisReplicationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisReplicationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisReplicationSpec) DeepCopyInto(out *RedisReplicationSpec) { + *out = *in + if in.Size != nil { + in, out := &in.Size, &out.Size + *out = new(int32) + **out = **in + } + in.KubernetesConfig.DeepCopyInto(&out.KubernetesConfig) + if in.RedisExporter != nil { + in, out := &in.RedisExporter, &out.RedisExporter + *out = new(commonv1beta2.RedisExporter) + (*in).DeepCopyInto(*out) + } + if in.RedisConfig != nil { + in, out := &in.RedisConfig, &out.RedisConfig + *out = new(commonv1beta2.RedisConfig) + (*in).DeepCopyInto(*out) + } + if in.Storage != nil { + in, out := &in.Storage, &out.Storage + *out = new(commonv1beta2.Storage) + (*in).DeepCopyInto(*out) + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.PodSecurityContext != nil { + in, out := &in.PodSecurityContext, &out.PodSecurityContext + *out = new(v1.PodSecurityContext) + (*in).DeepCopyInto(*out) + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(v1.SecurityContext) + (*in).DeepCopyInto(*out) + } + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(v1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = new([]v1.Toleration) + if **in != nil { + in, out := *in, *out + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(commonv1beta2.TLSConfig) + (*in).DeepCopyInto(*out) + } + if in.PodDisruptionBudget != nil { + in, out := &in.PodDisruptionBudget, &out.PodDisruptionBudget + *out = new(commonv1beta2.RedisPodDisruptionBudget) + (*in).DeepCopyInto(*out) + } + if in.ACL != nil { + in, out := &in.ACL, &out.ACL + *out = new(commonv1beta2.ACLConfig) + (*in).DeepCopyInto(*out) + } + if in.ReadinessProbe != nil { + in, out := &in.ReadinessProbe, &out.ReadinessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.LivenessProbe != nil { + in, out := &in.LivenessProbe, &out.LivenessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.InitContainer != nil { + in, out := &in.InitContainer, &out.InitContainer + *out = new(commonv1beta2.InitContainer) + (*in).DeepCopyInto(*out) + } + if in.Sidecars != nil { + in, out := &in.Sidecars, &out.Sidecars + *out = new([]commonv1beta2.Sidecar) + if **in != nil { + in, out := *in, *out + *out = make([]commonv1beta2.Sidecar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.ServiceAccountName != nil { + in, out := &in.ServiceAccountName, &out.ServiceAccountName + *out = new(string) + **out = **in + } + if in.TerminationGracePeriodSeconds != nil { + in, out := &in.TerminationGracePeriodSeconds, &out.TerminationGracePeriodSeconds + *out = new(int64) + **out = **in + } + if in.EnvVars != nil { + in, out := &in.EnvVars, &out.EnvVars + *out = new([]v1.EnvVar) + if **in != nil { + in, out := *in, *out + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.TopologySpreadConstrains != nil { + in, out := &in.TopologySpreadConstrains, &out.TopologySpreadConstrains + *out = make([]v1.TopologySpreadConstraint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.HostPort != nil { + in, out := &in.HostPort, &out.HostPort + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisReplicationSpec. +func (in *RedisReplicationSpec) DeepCopy() *RedisReplicationSpec { + if in == nil { + return nil + } + out := new(RedisReplicationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisReplicationStatus) DeepCopyInto(out *RedisReplicationStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisReplicationStatus. +func (in *RedisReplicationStatus) DeepCopy() *RedisReplicationStatus { + if in == nil { + return nil + } + out := new(RedisReplicationStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/vendored/redis-operator/redissentinel/v1beta2/README.md b/pkg/vendored/redis-operator/redissentinel/v1beta2/README.md new file mode 100644 index 00000000..a0abb9e4 --- /dev/null +++ b/pkg/vendored/redis-operator/redissentinel/v1beta2/README.md @@ -0,0 +1,41 @@ +# RedisSentinel API Types (v1beta2) + +## Source +- **Package**: `github.com/OT-CONTAINER-KIT/redis-operator/api/redissentinel/v1beta2` +- **Version**: v0.22.1 +- **Commit**: Corresponds to v0.22.1 release tag +- **Cloned From**: `/Users/j7m4/go/pkg/mod/github.com/!o!t-!c!o!n\!t\!a\!i\!n\!e\!r-\!k\!i\!t/redis-operator@v0.22.1/api/redissentinel/v1beta2/` + +## Contents +CRD types for Redis Sentinel (HA monitoring and failover): +- `RedisSentinel` - Main CRD type +- `RedisSentinelSpec` - Specification structure +- `RedisSentinelConfig` - Sentinel-specific configuration +- `RedisSentinelStatus` - Status structure +- Webhook validation (contains patched code) + +## Modifications + +### redissentinel_webhook.go +**Line 26-28**: Commented out unused webhook import +```go +// PATCHED: Commented out unused import after removing webhook.Validator +// "sigs.k8s.io/controller-runtime/pkg/webhook" +``` + +**Line 47-48**: Commented out problematic webhook.Validator interface check +```go +// PATCHED: Commented out due to controller-runtime incompatibility +// var _ webhook.Validator = &RedisSentinel{} +``` + +**Reason**: The original code references `webhook.Validator` which doesn't exist in controller-runtime. This line is only needed for webhook server compilation, which we don't use - we only need the CRD type definitions. + +### redissentinel_webhook_test.go +**Deleted**: Test file removed as it references internal redis-operator packages we don't need. + +### redissentinel_types.go +**Line 4**: Updated import path to use vendored common package +```go +common "github.com/wandb/operator/api/redis-operator/model/v1beta2" +``` diff --git a/pkg/vendored/redis-operator/redissentinel/v1beta2/groupversion_info.go b/pkg/vendored/redis-operator/redissentinel/v1beta2/groupversion_info.go new file mode 100644 index 00000000..65673400 --- /dev/null +++ b/pkg/vendored/redis-operator/redissentinel/v1beta2/groupversion_info.go @@ -0,0 +1,34 @@ +/* +Copyright 2020 Opstree Solutions. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta2 contains API Schema definitions for the redis v1beta2 API group +package v1beta2 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "redis.redis.opstreelabs.in", Version: "v1beta2"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/pkg/vendored/redis-operator/redissentinel/v1beta2/redissentinel_conversion.go b/pkg/vendored/redis-operator/redissentinel/v1beta2/redissentinel_conversion.go new file mode 100644 index 00000000..b4a8412a --- /dev/null +++ b/pkg/vendored/redis-operator/redissentinel/v1beta2/redissentinel_conversion.go @@ -0,0 +1,4 @@ +package v1beta2 + +// Hub marks this type as a conversion hub. +func (*RedisSentinel) Hub() {} diff --git a/pkg/vendored/redis-operator/redissentinel/v1beta2/redissentinel_types.go b/pkg/vendored/redis-operator/redissentinel/v1beta2/redissentinel_types.go new file mode 100644 index 00000000..14eabad9 --- /dev/null +++ b/pkg/vendored/redis-operator/redissentinel/v1beta2/redissentinel_types.go @@ -0,0 +1,68 @@ +package v1beta2 + +import ( + common "github.com/wandb/operator/pkg/vendored/redis-operator/common/v1beta2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type RedisSentinelSpec struct { + Size *int32 `json:"clusterSize"` + KubernetesConfig common.KubernetesConfig `json:"kubernetesConfig"` + RedisExporter *common.RedisExporter `json:"redisExporter,omitempty"` + RedisSentinelConfig *RedisSentinelConfig `json:"redisSentinelConfig,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + PodSecurityContext *corev1.PodSecurityContext `json:"podSecurityContext,omitempty"` + SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"` + PriorityClassName string `json:"priorityClassName,omitempty"` + Affinity *corev1.Affinity `json:"affinity,omitempty"` + Tolerations *[]corev1.Toleration `json:"tolerations,omitempty"` + TLS *common.TLSConfig `json:"TLS,omitempty"` + PodDisruptionBudget *common.RedisPodDisruptionBudget `json:"pdb,omitempty"` + ReadinessProbe *corev1.Probe `json:"readinessProbe,omitempty" protobuf:"bytes,11,opt,name=readinessProbe"` + LivenessProbe *corev1.Probe `json:"livenessProbe,omitempty" protobuf:"bytes,12,opt,name=livenessProbe"` + InitContainer *common.InitContainer `json:"initContainer,omitempty"` + Sidecars *[]common.Sidecar `json:"sidecars,omitempty"` + ServiceAccountName *string `json:"serviceAccountName,omitempty"` + TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty" protobuf:"varint,4,opt,name=terminationGracePeriodSeconds"` + EnvVars *[]corev1.EnvVar `json:"env,omitempty"` + VolumeMount *common.AdditionalVolume `json:"volumeMount,omitempty"` + TopologySpreadConstrains []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` + HostPort *int `json:"hostPort,omitempty"` +} + +func (cr *RedisSentinelSpec) GetSentinelCounts(t string) int32 { + replica := cr.Size + return *replica +} + +type RedisSentinelConfig struct { + common.RedisSentinelConfig `json:",inline"` +} + +type RedisSentinelStatus struct{} + +// Redis is the Schema for the redis API +type RedisSentinel struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RedisSentinelSpec `json:"spec"` + Status RedisSentinelStatus `json:"status,omitempty"` +} + +func (rs *RedisSentinel) GetStatefulSetName() string { + return rs.Name + "-sentinel" +} + +// RedisList contains a list of Redis +type RedisSentinelList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RedisSentinel `json:"items"` +} + +//nolint:gochecknoinits +func init() { + SchemeBuilder.Register(&RedisSentinel{}, &RedisSentinelList{}) +} diff --git a/pkg/vendored/redis-operator/redissentinel/v1beta2/redissentinel_webhook.go b/pkg/vendored/redis-operator/redissentinel/v1beta2/redissentinel_webhook.go new file mode 100644 index 00000000..f26abe00 --- /dev/null +++ b/pkg/vendored/redis-operator/redissentinel/v1beta2/redissentinel_webhook.go @@ -0,0 +1,102 @@ +/* +Copyright 2020 Opstree Solutions. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + // PATCHED: Commented out unused import after removing webhook.Validator + // "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +const ( + webhookPath = "/validate-redis-redis-opstreelabs-in-v1beta2-redissentinel" +) + +// log is for logging in this package. +var redissentinellog = logf.Log.WithName("redissentinel-v1beta2-validation") + +// SetupWebhookWithManager will setup the manager +func (r *RedisSentinel) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// PATCHED: Commented out due to controller-runtime incompatibility +// var _ webhook.Validator = &RedisSentinel{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *RedisSentinel) ValidateCreate() (admission.Warnings, error) { + redissentinellog.Info("validate create", "name", r.Name) + + return r.validate(nil) +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *RedisSentinel) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + redissentinellog.Info("validate update", "name", r.Name) + + return r.validate(old.(*RedisSentinel)) +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *RedisSentinel) ValidateDelete() (admission.Warnings, error) { + redissentinellog.Info("validate delete", "name", r.Name) + + return nil, nil +} + +// validate validates the Redis Sentinel CR +func (r *RedisSentinel) validate(_ *RedisSentinel) (admission.Warnings, error) { + var errors field.ErrorList + var warnings admission.Warnings + + if r.Spec.Size == nil { + return warnings, nil + } + + // Check if the Size is an odd number + if *r.Spec.Size%2 == 0 { + errors = append(errors, field.Invalid( + field.NewPath("spec").Child("clusterSize"), + *r.Spec.Size, + "Redis Sentinel cluster size must be an odd number for proper leader election", + )) + } + + if len(errors) == 0 { + return nil, nil + } + + return nil, apierrors.NewInvalid( + schema.GroupKind{Group: "redis.redis.opstreelabs.in", Kind: "RedisSentinel"}, + r.Name, + errors, + ) +} + +func (r *RedisSentinel) WebhookPath() string { + return webhookPath +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! diff --git a/pkg/vendored/redis-operator/redissentinel/v1beta2/zz_generated.deepcopy.go b/pkg/vendored/redis-operator/redissentinel/v1beta2/zz_generated.deepcopy.go new file mode 100644 index 00000000..fce7efbe --- /dev/null +++ b/pkg/vendored/redis-operator/redissentinel/v1beta2/zz_generated.deepcopy.go @@ -0,0 +1,255 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta2 + +import ( + commonv1beta2 "github.com/wandb/operator/pkg/vendored/redis-operator/common/v1beta2" + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSentinel) DeepCopyInto(out *RedisSentinel) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSentinel. +func (in *RedisSentinel) DeepCopy() *RedisSentinel { + if in == nil { + return nil + } + out := new(RedisSentinel) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisSentinel) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSentinelConfig) DeepCopyInto(out *RedisSentinelConfig) { + *out = *in + in.RedisSentinelConfig.DeepCopyInto(&out.RedisSentinelConfig) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSentinelConfig. +func (in *RedisSentinelConfig) DeepCopy() *RedisSentinelConfig { + if in == nil { + return nil + } + out := new(RedisSentinelConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSentinelList) DeepCopyInto(out *RedisSentinelList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RedisSentinel, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSentinelList. +func (in *RedisSentinelList) DeepCopy() *RedisSentinelList { + if in == nil { + return nil + } + out := new(RedisSentinelList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisSentinelList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSentinelSpec) DeepCopyInto(out *RedisSentinelSpec) { + *out = *in + if in.Size != nil { + in, out := &in.Size, &out.Size + *out = new(int32) + **out = **in + } + in.KubernetesConfig.DeepCopyInto(&out.KubernetesConfig) + if in.RedisExporter != nil { + in, out := &in.RedisExporter, &out.RedisExporter + *out = new(commonv1beta2.RedisExporter) + (*in).DeepCopyInto(*out) + } + if in.RedisSentinelConfig != nil { + in, out := &in.RedisSentinelConfig, &out.RedisSentinelConfig + *out = new(RedisSentinelConfig) + (*in).DeepCopyInto(*out) + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.PodSecurityContext != nil { + in, out := &in.PodSecurityContext, &out.PodSecurityContext + *out = new(v1.PodSecurityContext) + (*in).DeepCopyInto(*out) + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(v1.SecurityContext) + (*in).DeepCopyInto(*out) + } + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(v1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = new([]v1.Toleration) + if **in != nil { + in, out := *in, *out + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(commonv1beta2.TLSConfig) + (*in).DeepCopyInto(*out) + } + if in.PodDisruptionBudget != nil { + in, out := &in.PodDisruptionBudget, &out.PodDisruptionBudget + *out = new(commonv1beta2.RedisPodDisruptionBudget) + (*in).DeepCopyInto(*out) + } + if in.ReadinessProbe != nil { + in, out := &in.ReadinessProbe, &out.ReadinessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.LivenessProbe != nil { + in, out := &in.LivenessProbe, &out.LivenessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.InitContainer != nil { + in, out := &in.InitContainer, &out.InitContainer + *out = new(commonv1beta2.InitContainer) + (*in).DeepCopyInto(*out) + } + if in.Sidecars != nil { + in, out := &in.Sidecars, &out.Sidecars + *out = new([]commonv1beta2.Sidecar) + if **in != nil { + in, out := *in, *out + *out = make([]commonv1beta2.Sidecar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.ServiceAccountName != nil { + in, out := &in.ServiceAccountName, &out.ServiceAccountName + *out = new(string) + **out = **in + } + if in.TerminationGracePeriodSeconds != nil { + in, out := &in.TerminationGracePeriodSeconds, &out.TerminationGracePeriodSeconds + *out = new(int64) + **out = **in + } + if in.EnvVars != nil { + in, out := &in.EnvVars, &out.EnvVars + *out = new([]v1.EnvVar) + if **in != nil { + in, out := *in, *out + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.VolumeMount != nil { + in, out := &in.VolumeMount, &out.VolumeMount + *out = new(commonv1beta2.AdditionalVolume) + (*in).DeepCopyInto(*out) + } + if in.TopologySpreadConstrains != nil { + in, out := &in.TopologySpreadConstrains, &out.TopologySpreadConstrains + *out = make([]v1.TopologySpreadConstraint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.HostPort != nil { + in, out := &in.HostPort, &out.HostPort + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSentinelSpec. +func (in *RedisSentinelSpec) DeepCopy() *RedisSentinelSpec { + if in == nil { + return nil + } + out := new(RedisSentinelSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisSentinelStatus) DeepCopyInto(out *RedisSentinelStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisSentinelStatus. +func (in *RedisSentinelStatus) DeepCopy() *RedisSentinelStatus { + if in == nil { + return nil + } + out := new(RedisSentinelStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/vendored/seaweedfs-operator/README.md b/pkg/vendored/seaweedfs-operator/README.md new file mode 100644 index 00000000..e0a84175 --- /dev/null +++ b/pkg/vendored/seaweedfs-operator/README.md @@ -0,0 +1,48 @@ +# SeaweedFS Operator API Vendored Code + +This directory contains vendored API types from the [SeaweedFS Operator](https://github.com/seaweedfs/seaweedfs-operator) project. + +## Source + +- **Repository**: https://github.com/seaweedfs/seaweedfs-operator +- **Version**: v0.1.13 +- **Date Vendored**: 2026-04-13 + +## Reason for Vendoring + +To have full control over the CRD API types and avoid unexpected breaking changes when the upstream operator updates. This allows the W&B operator to manage SeaweedFS clusters while controlling when and how we adopt upstream changes. Vendoring also avoids pulling in the full SeaweedFS server dependency tree via go.mod. + +## Changes Made + +### Removed Code Generation Annotations + +All `+kubebuilder:*` annotations have been removed from vendored types. These annotations are only needed when running code generators, which we do not do for vendored APIs. + +### Removed Files + +- `component_accessor.go` - Runtime helpers not needed for CR creation/status reading +- `seaweed_webhook.go` - Webhook logic not needed +- `seaweed_webhook_test.go` - Tests + +### Added Files + +- `vendored_helpers.go` - GroupName constant + +## What Was Vendored + +### CRDs + +- `crds/seaweed.seaweedfs.com_seaweeds.yaml` + - **Source**: https://github.com/seaweedfs/seaweedfs-operator/config/crd/bases/seaweed.seaweedfs.com_seaweeds.yaml + +### API Types + +- `seaweed.seaweedfs.com/v1/` - SeaweedFS CRD types + - `types.go` - Main Seaweed type definitions + - `groupversion_info.go` - API group registration and scheme builder + - `vendored_helpers.go` - Helper constants + - `zz_generated.deepcopy.go` - Generated DeepCopy methods + +## License + +The vendored code is licensed under the Apache License 2.0 from the SeaweedFS Operator project. diff --git a/pkg/vendored/seaweedfs-operator/crds/seaweed.seaweedfs.com_seaweeds.yaml b/pkg/vendored/seaweedfs-operator/crds/seaweed.seaweedfs.com_seaweeds.yaml new file mode 100644 index 00000000..9b08316a --- /dev/null +++ b/pkg/vendored/seaweedfs-operator/crds/seaweed.seaweedfs.com_seaweeds.yaml @@ -0,0 +1,10727 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: seaweeds.seaweed.seaweedfs.com +spec: + group: seaweed.seaweedfs.com + names: + kind: Seaweed + listKind: SeaweedList + plural: seaweeds + singular: seaweed + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + admin: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + annotations: + additionalProperties: + type: string + type: object + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + credentialsSecret: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + extraArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + hostNetwork: + type: boolean + imagePullPolicy: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + ingress: + properties: + annotations: + additionalProperties: + type: string + type: object + className: + type: string + enabled: + type: boolean + host: + type: string + path: + default: / + type: string + tls: + items: + properties: + hosts: + items: + type: string + type: array + secretName: + type: string + type: object + type: array + type: object + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + metricsPort: + type: integer + nodeSelector: + additionalProperties: + type: string + type: object + priorityClassName: + type: string + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + schedulerName: + type: string + service: + properties: + annotations: + additionalProperties: + type: string + type: object + clusterIP: + type: string + loadBalancerIP: + type: string + type: + type: string + type: object + statefulSetUpdateStrategy: + type: string + terminationGracePeriodSeconds: + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + type: object + type: array + version: + type: string + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + annotations: + additionalProperties: + type: string + type: object + enablePVReclaim: + type: boolean + filer: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + annotations: + additionalProperties: + type: string + type: object + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + config: + type: string + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + extraArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + hostNetwork: + type: boolean + iam: + default: true + type: boolean + iceberg: + properties: + enabled: + default: true + type: boolean + port: + maximum: 65535 + minimum: 1 + type: integer + type: object + imagePullPolicy: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + ingress: + properties: + annotations: + additionalProperties: + type: string + type: object + className: + type: string + enabled: + type: boolean + host: + type: string + path: + default: / + type: string + tls: + items: + properties: + hosts: + items: + type: string + type: array + secretName: + type: string + type: object + type: array + type: object + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + maxMB: + type: integer + metricsPort: + type: integer + nodeSelector: + additionalProperties: + type: string + type: object + persistence: + properties: + accessModes: + default: + - ReadWriteOnce + items: + type: string + type: array + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + enabled: + default: false + type: boolean + existingClaim: + type: string + mountPath: + default: /data + type: string + resources: + default: + requests: + storage: 4Gi + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + subPath: + default: "" + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + priorityClassName: + type: string + replicas: + minimum: 1 + type: integer + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + s3: + properties: + configSecret: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + enabled: + default: true + type: boolean + type: object + s3Ingress: + properties: + annotations: + additionalProperties: + type: string + type: object + className: + type: string + enabled: + type: boolean + host: + type: string + path: + default: / + type: string + tls: + items: + properties: + hosts: + items: + type: string + type: array + secretName: + type: string + type: object + type: array + type: object + schedulerName: + type: string + service: + properties: + annotations: + additionalProperties: + type: string + type: object + clusterIP: + type: string + loadBalancerIP: + type: string + type: + type: string + type: object + statefulSetUpdateStrategy: + type: string + terminationGracePeriodSeconds: + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + type: object + type: array + version: + type: string + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + required: + - replicas + type: object + hostNetwork: + type: boolean + hostSuffix: + type: string + image: + type: string + imagePullPolicy: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + master: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + annotations: + additionalProperties: + type: string + type: object + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + concurrentStart: + type: boolean + config: + type: string + defaultReplication: + type: string + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + extraArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + garbageThreshold: + type: string + hostNetwork: + type: boolean + imagePullPolicy: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + ingress: + properties: + annotations: + additionalProperties: + type: string + type: object + className: + type: string + enabled: + type: boolean + host: + type: string + path: + default: / + type: string + tls: + items: + properties: + hosts: + items: + type: string + type: array + secretName: + type: string + type: object + type: array + type: object + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + metricsPort: + type: integer + nodeSelector: + additionalProperties: + type: string + type: object + priorityClassName: + type: string + pulseSeconds: + type: integer + replicas: + minimum: 1 + type: integer + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + schedulerName: + type: string + service: + properties: + annotations: + additionalProperties: + type: string + type: object + clusterIP: + type: string + loadBalancerIP: + type: string + type: + type: string + type: object + statefulSetUpdateStrategy: + type: string + terminationGracePeriodSeconds: + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + type: object + type: array + version: + type: string + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + volumePreallocate: + type: boolean + volumeSizeLimitMB: + type: integer + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + required: + - replicas + type: object + metricsAddress: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + pvReclaimPolicy: + type: string + s3: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + annotations: + additionalProperties: + type: string + type: object + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + configSecret: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + domainName: + type: string + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + extraArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + hostNetwork: + type: boolean + iam: + default: true + type: boolean + imagePullPolicy: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + ingress: + properties: + annotations: + additionalProperties: + type: string + type: object + className: + type: string + enabled: + type: boolean + host: + type: string + path: + default: / + type: string + tls: + items: + properties: + hosts: + items: + type: string + type: array + secretName: + type: string + type: object + type: array + type: object + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + metricsPort: + type: integer + nodeSelector: + additionalProperties: + type: string + type: object + port: + type: integer + priorityClassName: + type: string + replicas: + default: 1 + minimum: 1 + type: integer + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + schedulerName: + type: string + service: + properties: + annotations: + additionalProperties: + type: string + type: object + clusterIP: + type: string + loadBalancerIP: + type: string + type: + type: string + type: object + statefulSetUpdateStrategy: + type: string + terminationGracePeriodSeconds: + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + type: object + type: array + version: + type: string + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + required: + - replicas + type: object + schedulerName: + type: string + statefulSetUpdateStrategy: + type: string + tls: + properties: + enabled: + type: boolean + issuerRef: + properties: + group: + default: cert-manager.io + type: string + kind: + default: Issuer + enum: + - Issuer + - ClusterIssuer + type: string + name: + type: string + required: + - name + type: object + type: object + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + type: object + type: array + version: + type: string + volume: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + annotations: + additionalProperties: + type: string + type: object + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + compactionMBps: + type: integer + dataCenter: + type: string + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + extraArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + fileSizeLimitMB: + type: integer + fixJpgOrientation: + type: boolean + hostNetwork: + type: boolean + idleTimeout: + type: integer + imagePullPolicy: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + ingress: + properties: + annotations: + additionalProperties: + type: string + type: object + className: + type: string + enabled: + type: boolean + host: + type: string + path: + default: / + type: string + tls: + items: + properties: + hosts: + items: + type: string + type: array + secretName: + type: string + type: object + type: array + type: object + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + maxVolumeCounts: + type: integer + metricsPort: + type: integer + minFreeSpacePercent: + type: integer + nodeSelector: + additionalProperties: + type: string + type: object + priorityClassName: + type: string + rack: + type: string + replicas: + minimum: 0 + type: integer + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + schedulerName: + type: string + service: + properties: + annotations: + additionalProperties: + type: string + type: object + clusterIP: + type: string + loadBalancerIP: + type: string + type: + type: string + type: object + statefulSetUpdateStrategy: + type: string + storageClassName: + type: string + terminationGracePeriodSeconds: + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + type: object + type: array + version: + type: string + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + required: + - replicas + type: object + volumeServerDiskCount: + type: integer + volumeTopology: + additionalProperties: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + annotations: + additionalProperties: + type: string + type: object + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + compactionMBps: + type: integer + dataCenter: + type: string + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + extraArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + fileSizeLimitMB: + type: integer + fixJpgOrientation: + type: boolean + hostNetwork: + type: boolean + idleTimeout: + type: integer + imagePullPolicy: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + maxVolumeCounts: + type: integer + metricsPort: + type: integer + minFreeSpacePercent: + type: integer + nodeSelector: + additionalProperties: + type: string + type: object + priorityClassName: + type: string + rack: + type: string + replicas: + minimum: 0 + type: integer + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + schedulerName: + type: string + service: + properties: + annotations: + additionalProperties: + type: string + type: object + clusterIP: + type: string + loadBalancerIP: + type: string + type: + type: string + type: object + statefulSetUpdateStrategy: + type: string + storageClassName: + type: string + terminationGracePeriodSeconds: + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + type: object + type: array + version: + type: string + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + required: + - dataCenter + - rack + - replicas + type: object + type: object + worker: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + annotations: + additionalProperties: + type: string + type: object + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + extraArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + hostNetwork: + type: boolean + imagePullPolicy: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + jobType: + default: all + type: string + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + maxDetect: + minimum: 1 + type: integer + maxExecute: + minimum: 1 + type: integer + metricsPort: + type: integer + nodeSelector: + additionalProperties: + type: string + type: object + persistence: + properties: + accessModes: + default: + - ReadWriteOnce + items: + type: string + type: array + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + enabled: + default: false + type: boolean + existingClaim: + type: string + mountPath: + default: /data + type: string + resources: + default: + requests: + storage: 4Gi + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + subPath: + default: "" + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + priorityClassName: + type: string + replicas: + default: 1 + minimum: 1 + type: integer + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + schedulerName: + type: string + statefulSetUpdateStrategy: + type: string + terminationGracePeriodSeconds: + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + type: object + type: array + version: + type: string + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + type: string + kind: + type: string + readOnly: + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + type: string + lun: + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + required: + - replicas + type: object + type: object + status: + properties: + admin: + properties: + readyReplicas: + minimum: 0 + type: integer + replicas: + minimum: 0 + type: integer + type: object + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + filer: + properties: + readyReplicas: + minimum: 0 + type: integer + replicas: + minimum: 0 + type: integer + type: object + master: + properties: + readyReplicas: + minimum: 0 + type: integer + replicas: + minimum: 0 + type: integer + type: object + observedGeneration: + type: integer + s3: + properties: + readyReplicas: + minimum: 0 + type: integer + replicas: + minimum: 0 + type: integer + type: object + volume: + properties: + readyReplicas: + minimum: 0 + type: integer + replicas: + minimum: 0 + type: integer + type: object + worker: + properties: + readyReplicas: + minimum: 0 + type: integer + replicas: + minimum: 0 + type: integer + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/groupversion_info.go b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/groupversion_info.go new file mode 100644 index 00000000..ce077e8e --- /dev/null +++ b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/groupversion_info.go @@ -0,0 +1,31 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "seaweed.seaweedfs.com", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/types.go b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/types.go new file mode 100644 index 00000000..f8222941 --- /dev/null +++ b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/types.go @@ -0,0 +1,305 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Constants +const ( + GRPCPortDelta = 10000 + + MasterHTTPPort = 9333 + VolumeHTTPPort = 8444 + FilerHTTPPort = 8888 + FilerS3Port = 8333 + FilerIcebergPort = 8181 + AdminHTTPPort = 23646 + WorkerMetricsPort = 9101 + + MasterGRPCPort = MasterHTTPPort + GRPCPortDelta + VolumeGRPCPort = VolumeHTTPPort + GRPCPortDelta + FilerGRPCPort = FilerHTTPPort + GRPCPortDelta + AdminGRPCPort = AdminHTTPPort + GRPCPortDelta +) + +type IngressSpec struct { + Enabled bool `json:"enabled,omitempty"` + ClassName *string `json:"className,omitempty"` + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + TLS []IngressTLS `json:"tls,omitempty"` +} + +type IngressTLS struct { + Hosts []string `json:"hosts,omitempty"` + SecretName string `json:"secretName,omitempty"` +} + +type TLSSpec struct { + Enabled bool `json:"enabled,omitempty"` + IssuerRef *TLSIssuerRef `json:"issuerRef,omitempty"` +} + +type TLSIssuerRef struct { + Name string `json:"name"` + Kind string `json:"kind,omitempty"` + Group string `json:"group,omitempty"` +} + +type SeaweedSpec struct { + TLS *TLSSpec `json:"tls,omitempty"` + MetricsAddress string `json:"metricsAddress,omitempty"` + Image string `json:"image,omitempty"` + Version string `json:"version,omitempty"` + + Master *MasterSpec `json:"master,omitempty"` + Volume *VolumeSpec `json:"volume,omitempty"` + VolumeTopology map[string]*VolumeTopologySpec `json:"volumeTopology,omitempty"` + Filer *FilerSpec `json:"filer,omitempty"` + Admin *AdminSpec `json:"admin,omitempty"` + Worker *WorkerSpec `json:"worker,omitempty"` + S3 *S3GatewaySpec `json:"s3,omitempty"` + + SchedulerName string `json:"schedulerName,omitempty"` + PVReclaimPolicy *corev1.PersistentVolumeReclaimPolicy `json:"pvReclaimPolicy,omitempty"` + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + EnablePVReclaim *bool `json:"enablePVReclaim,omitempty"` + HostNetwork *bool `json:"hostNetwork,omitempty"` + Affinity *corev1.Affinity `json:"affinity,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + StatefulSetUpdateStrategy appsv1.StatefulSetUpdateStrategyType `json:"statefulSetUpdateStrategy,omitempty"` + VolumeServerDiskCount *int32 `json:"volumeServerDiskCount,omitempty"` + HostSuffix *string `json:"hostSuffix,omitempty"` +} + +// SeaweedStatus defines the observed state of Seaweed +type SeaweedStatus struct { + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + Master ComponentStatus `json:"master,omitempty"` + Volume ComponentStatus `json:"volume,omitempty"` + Filer ComponentStatus `json:"filer,omitempty"` + Admin ComponentStatus `json:"admin,omitempty"` + Worker ComponentStatus `json:"worker,omitempty"` + S3 ComponentStatus `json:"s3,omitempty"` +} + +// ComponentStatus represents the status of a seaweedfs component +type ComponentStatus struct { + Replicas int32 `json:"replicas,omitempty"` + ReadyReplicas int32 `json:"readyReplicas,omitempty"` +} + +// MasterSpec is the spec for masters +type MasterSpec struct { + ComponentSpec `json:",inline"` + corev1.ResourceRequirements `json:",inline"` + + Replicas int32 `json:"replicas"` + Service *ServiceSpec `json:"service,omitempty"` + Config *string `json:"config,omitempty"` + + MetricsPort *int32 `json:"metricsPort,omitempty"` + VolumePreallocate *bool `json:"volumePreallocate,omitempty"` + VolumeSizeLimitMB *int32 `json:"volumeSizeLimitMB,omitempty"` + GarbageThreshold *string `json:"garbageThreshold,omitempty"` + PulseSeconds *int32 `json:"pulseSeconds,omitempty"` + DefaultReplication *string `json:"defaultReplication,omitempty"` + ConcurrentStart *bool `json:"concurrentStart,omitempty"` + Ingress *IngressSpec `json:"ingress,omitempty"` +} + +type VolumeServerConfig struct { + ComponentSpec `json:",inline"` + corev1.ResourceRequirements `json:",inline"` + + Service *ServiceSpec `json:"service,omitempty"` + StorageClassName *string `json:"storageClassName,omitempty"` + + MetricsPort *int32 `json:"metricsPort,omitempty"` + CompactionMBps *int32 `json:"compactionMBps,omitempty"` + FileSizeLimitMB *int32 `json:"fileSizeLimitMB,omitempty"` + FixJpgOrientation *bool `json:"fixJpgOrientation,omitempty"` + IdleTimeout *int32 `json:"idleTimeout,omitempty"` + MaxVolumeCounts *int32 `json:"maxVolumeCounts,omitempty"` + MinFreeSpacePercent *int32 `json:"minFreeSpacePercent,omitempty"` +} + +// VolumeSpec is the spec for volume servers +type VolumeSpec struct { + VolumeServerConfig `json:",inline"` + + Replicas int32 `json:"replicas"` + Rack *string `json:"rack,omitempty"` + DataCenter *string `json:"dataCenter,omitempty"` + Ingress *IngressSpec `json:"ingress,omitempty"` +} + +// VolumeTopologySpec defines a volume server group with specific topology placement +type VolumeTopologySpec struct { + VolumeServerConfig `json:",inline"` + + Replicas int32 `json:"replicas"` + Rack string `json:"rack"` + DataCenter string `json:"dataCenter"` +} + +// S3Config defines the S3 configuration (deprecated: prefer S3GatewaySpec) +type S3Config struct { + Enabled bool `json:"enabled,omitempty"` + ConfigSecret *corev1.SecretKeySelector `json:"configSecret,omitempty"` +} + +// S3GatewaySpec defines a standalone S3 gateway Deployment +type S3GatewaySpec struct { + ComponentSpec `json:",inline"` + corev1.ResourceRequirements `json:",inline"` + + Replicas int32 `json:"replicas"` + Service *ServiceSpec `json:"service,omitempty"` + ConfigSecret *corev1.SecretKeySelector `json:"configSecret,omitempty"` + MetricsPort *int32 `json:"metricsPort,omitempty"` + Port *int32 `json:"port,omitempty"` + DomainName *string `json:"domainName,omitempty"` + IAM bool `json:"iam,omitempty"` + Ingress *IngressSpec `json:"ingress,omitempty"` +} + +// IcebergConfig defines the Iceberg catalog REST API configuration +type IcebergConfig struct { + Enabled bool `json:"enabled,omitempty"` + Port *int32 `json:"port,omitempty"` +} + +// IcebergEffectivePort returns the port to use for the Iceberg catalog REST API. +func (c *IcebergConfig) IcebergEffectivePort() int32 { + if c.Port != nil { + return *c.Port + } + return FilerIcebergPort +} + +// FilerSpec is the spec for filers +type FilerSpec struct { + ComponentSpec `json:",inline"` + corev1.ResourceRequirements `json:",inline"` + + Replicas int32 `json:"replicas"` + Service *ServiceSpec `json:"service,omitempty"` + Config *string `json:"config,omitempty"` + MetricsPort *int32 `json:"metricsPort,omitempty"` + Persistence *PersistenceSpec `json:"persistence,omitempty"` + + MaxMB *int32 `json:"maxMB,omitempty"` + S3 *S3Config `json:"s3,omitempty"` + IAM bool `json:"iam,omitempty"` + Iceberg *IcebergConfig `json:"iceberg,omitempty"` + Ingress *IngressSpec `json:"ingress,omitempty"` + S3Ingress *IngressSpec `json:"s3Ingress,omitempty"` +} + +// AdminSpec is the spec for the admin server +type AdminSpec struct { + ComponentSpec `json:",inline"` + corev1.ResourceRequirements `json:",inline"` + + Service *ServiceSpec `json:"service,omitempty"` + MetricsPort *int32 `json:"metricsPort,omitempty"` + CredentialsSecret *corev1.LocalObjectReference `json:"credentialsSecret,omitempty"` + Ingress *IngressSpec `json:"ingress,omitempty"` +} + +// WorkerSpec is the spec for worker processes +type WorkerSpec struct { + ComponentSpec `json:",inline"` + corev1.ResourceRequirements `json:",inline"` + + Replicas int32 `json:"replicas"` + MetricsPort *int32 `json:"metricsPort,omitempty"` + Persistence *PersistenceSpec `json:"persistence,omitempty"` + JobType *string `json:"jobType,omitempty"` + MaxDetect *int32 `json:"maxDetect,omitempty"` + MaxExecute *int32 `json:"maxExecute,omitempty"` +} + +// ComponentSpec is the base spec of each component +type ComponentSpec struct { + Version *string `json:"version,omitempty"` + ImagePullPolicy *corev1.PullPolicy `json:"imagePullPolicy,omitempty"` + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + HostNetwork *bool `json:"hostNetwork,omitempty"` + Affinity *corev1.Affinity `json:"affinity,omitempty"` + PriorityClassName *string `json:"priorityClassName,omitempty"` + SchedulerName *string `json:"schedulerName,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + Env []corev1.EnvVar `json:"env,omitempty"` + TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty"` + StatefulSetUpdateStrategy appsv1.StatefulSetUpdateStrategyType `json:"statefulSetUpdateStrategy,omitempty"` + Volumes []corev1.Volume `json:"volumes,omitempty"` + VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` + ExtraArgs []string `json:"extraArgs,omitempty"` +} + +// ServiceSpec is a subset of the original k8s spec +type ServiceSpec struct { + Type corev1.ServiceType `json:"type,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + LoadBalancerIP *string `json:"loadBalancerIP,omitempty"` + ClusterIP *string `json:"clusterIP,omitempty"` +} + +type PersistenceSpec struct { + Enabled bool `json:"enabled,omitempty"` + ExistingClaim *string `json:"existingClaim,omitempty"` + MountPath *string `json:"mountPath,omitempty"` + SubPath *string `json:"subPath,omitempty"` + AccessModes []corev1.PersistentVolumeAccessMode `json:"accessModes,omitempty"` + Selector *metav1.LabelSelector `json:"selector,omitempty"` + Resources corev1.VolumeResourceRequirements `json:"resources,omitempty"` + VolumeName string `json:"volumeName,omitempty"` + StorageClassName *string `json:"storageClassName,omitempty"` + VolumeMode *corev1.PersistentVolumeMode `json:"volumeMode,omitempty"` + DataSource *corev1.TypedLocalObjectReference `json:"dataSource,omitempty"` +} + +// Seaweed is the Schema for the seaweeds API +type Seaweed struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SeaweedSpec `json:"spec,omitempty"` + Status SeaweedStatus `json:"status,omitempty"` +} + +// SeaweedList contains a list of Seaweed +type SeaweedList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Seaweed `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Seaweed{}, &SeaweedList{}) +} diff --git a/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/vendored_helpers.go b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/vendored_helpers.go new file mode 100644 index 00000000..fe36f0b7 --- /dev/null +++ b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/vendored_helpers.go @@ -0,0 +1,5 @@ +// Vendored helper constants to avoid dependencies on internal SeaweedFS packages +package v1 + +// GroupName for SeaweedFS CRDs +const GroupName = "seaweed.seaweedfs.com" diff --git a/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/zz_generated.deepcopy.go b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/zz_generated.deepcopy.go new file mode 100644 index 00000000..b9d323a9 --- /dev/null +++ b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/zz_generated.deepcopy.go @@ -0,0 +1,934 @@ +//go:build !ignore_autogenerated + +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdminSpec) DeepCopyInto(out *AdminSpec) { + *out = *in + in.ComponentSpec.DeepCopyInto(&out.ComponentSpec) + in.ResourceRequirements.DeepCopyInto(&out.ResourceRequirements) + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(ServiceSpec) + (*in).DeepCopyInto(*out) + } + if in.MetricsPort != nil { + in, out := &in.MetricsPort, &out.MetricsPort + *out = new(int32) + **out = **in + } + if in.CredentialsSecret != nil { + in, out := &in.CredentialsSecret, &out.CredentialsSecret + *out = new(corev1.LocalObjectReference) + **out = **in + } + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = new(IngressSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdminSpec. +func (in *AdminSpec) DeepCopy() *AdminSpec { + if in == nil { + return nil + } + out := new(AdminSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComponentSpec) DeepCopyInto(out *ComponentSpec) { + *out = *in + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } + if in.ImagePullPolicy != nil { + in, out := &in.ImagePullPolicy, &out.ImagePullPolicy + *out = new(corev1.PullPolicy) + **out = **in + } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]corev1.LocalObjectReference, len(*in)) + copy(*out, *in) + } + if in.HostNetwork != nil { + in, out := &in.HostNetwork, &out.HostNetwork + *out = new(bool) + **out = **in + } + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(corev1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.PriorityClassName != nil { + in, out := &in.PriorityClassName, &out.PriorityClassName + *out = new(string) + **out = **in + } + if in.SchedulerName != nil { + in, out := &in.SchedulerName, &out.SchedulerName + *out = new(string) + **out = **in + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]corev1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.TerminationGracePeriodSeconds != nil { + in, out := &in.TerminationGracePeriodSeconds, &out.TerminationGracePeriodSeconds + *out = new(int64) + **out = **in + } + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make([]corev1.Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.VolumeMounts != nil { + in, out := &in.VolumeMounts, &out.VolumeMounts + *out = make([]corev1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ExtraArgs != nil { + in, out := &in.ExtraArgs, &out.ExtraArgs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentSpec. +func (in *ComponentSpec) DeepCopy() *ComponentSpec { + if in == nil { + return nil + } + out := new(ComponentSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComponentStatus) DeepCopyInto(out *ComponentStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentStatus. +func (in *ComponentStatus) DeepCopy() *ComponentStatus { + if in == nil { + return nil + } + out := new(ComponentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FilerSpec) DeepCopyInto(out *FilerSpec) { + *out = *in + in.ComponentSpec.DeepCopyInto(&out.ComponentSpec) + in.ResourceRequirements.DeepCopyInto(&out.ResourceRequirements) + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(ServiceSpec) + (*in).DeepCopyInto(*out) + } + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = new(string) + **out = **in + } + if in.MetricsPort != nil { + in, out := &in.MetricsPort, &out.MetricsPort + *out = new(int32) + **out = **in + } + if in.Persistence != nil { + in, out := &in.Persistence, &out.Persistence + *out = new(PersistenceSpec) + (*in).DeepCopyInto(*out) + } + if in.MaxMB != nil { + in, out := &in.MaxMB, &out.MaxMB + *out = new(int32) + **out = **in + } + if in.S3 != nil { + in, out := &in.S3, &out.S3 + *out = new(S3Config) + (*in).DeepCopyInto(*out) + } + if in.Iceberg != nil { + in, out := &in.Iceberg, &out.Iceberg + *out = new(IcebergConfig) + (*in).DeepCopyInto(*out) + } + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = new(IngressSpec) + (*in).DeepCopyInto(*out) + } + if in.S3Ingress != nil { + in, out := &in.S3Ingress, &out.S3Ingress + *out = new(IngressSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FilerSpec. +func (in *FilerSpec) DeepCopy() *FilerSpec { + if in == nil { + return nil + } + out := new(FilerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IcebergConfig) DeepCopyInto(out *IcebergConfig) { + *out = *in + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IcebergConfig. +func (in *IcebergConfig) DeepCopy() *IcebergConfig { + if in == nil { + return nil + } + out := new(IcebergConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressSpec) DeepCopyInto(out *IngressSpec) { + *out = *in + if in.ClassName != nil { + in, out := &in.ClassName, &out.ClassName + *out = new(string) + **out = **in + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = make([]IngressTLS, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressSpec. +func (in *IngressSpec) DeepCopy() *IngressSpec { + if in == nil { + return nil + } + out := new(IngressSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressTLS) DeepCopyInto(out *IngressTLS) { + *out = *in + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressTLS. +func (in *IngressTLS) DeepCopy() *IngressTLS { + if in == nil { + return nil + } + out := new(IngressTLS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MasterSpec) DeepCopyInto(out *MasterSpec) { + *out = *in + in.ComponentSpec.DeepCopyInto(&out.ComponentSpec) + in.ResourceRequirements.DeepCopyInto(&out.ResourceRequirements) + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(ServiceSpec) + (*in).DeepCopyInto(*out) + } + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = new(string) + **out = **in + } + if in.MetricsPort != nil { + in, out := &in.MetricsPort, &out.MetricsPort + *out = new(int32) + **out = **in + } + if in.VolumePreallocate != nil { + in, out := &in.VolumePreallocate, &out.VolumePreallocate + *out = new(bool) + **out = **in + } + if in.VolumeSizeLimitMB != nil { + in, out := &in.VolumeSizeLimitMB, &out.VolumeSizeLimitMB + *out = new(int32) + **out = **in + } + if in.GarbageThreshold != nil { + in, out := &in.GarbageThreshold, &out.GarbageThreshold + *out = new(string) + **out = **in + } + if in.PulseSeconds != nil { + in, out := &in.PulseSeconds, &out.PulseSeconds + *out = new(int32) + **out = **in + } + if in.DefaultReplication != nil { + in, out := &in.DefaultReplication, &out.DefaultReplication + *out = new(string) + **out = **in + } + if in.ConcurrentStart != nil { + in, out := &in.ConcurrentStart, &out.ConcurrentStart + *out = new(bool) + **out = **in + } + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = new(IngressSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MasterSpec. +func (in *MasterSpec) DeepCopy() *MasterSpec { + if in == nil { + return nil + } + out := new(MasterSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PersistenceSpec) DeepCopyInto(out *PersistenceSpec) { + *out = *in + if in.ExistingClaim != nil { + in, out := &in.ExistingClaim, &out.ExistingClaim + *out = new(string) + **out = **in + } + if in.MountPath != nil { + in, out := &in.MountPath, &out.MountPath + *out = new(string) + **out = **in + } + if in.SubPath != nil { + in, out := &in.SubPath, &out.SubPath + *out = new(string) + **out = **in + } + if in.AccessModes != nil { + in, out := &in.AccessModes, &out.AccessModes + *out = make([]corev1.PersistentVolumeAccessMode, len(*in)) + copy(*out, *in) + } + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } + in.Resources.DeepCopyInto(&out.Resources) + if in.StorageClassName != nil { + in, out := &in.StorageClassName, &out.StorageClassName + *out = new(string) + **out = **in + } + if in.VolumeMode != nil { + in, out := &in.VolumeMode, &out.VolumeMode + *out = new(corev1.PersistentVolumeMode) + **out = **in + } + if in.DataSource != nil { + in, out := &in.DataSource, &out.DataSource + *out = new(corev1.TypedLocalObjectReference) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PersistenceSpec. +func (in *PersistenceSpec) DeepCopy() *PersistenceSpec { + if in == nil { + return nil + } + out := new(PersistenceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3Config) DeepCopyInto(out *S3Config) { + *out = *in + if in.ConfigSecret != nil { + in, out := &in.ConfigSecret, &out.ConfigSecret + *out = new(corev1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3Config. +func (in *S3Config) DeepCopy() *S3Config { + if in == nil { + return nil + } + out := new(S3Config) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3GatewaySpec) DeepCopyInto(out *S3GatewaySpec) { + *out = *in + in.ComponentSpec.DeepCopyInto(&out.ComponentSpec) + in.ResourceRequirements.DeepCopyInto(&out.ResourceRequirements) + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(ServiceSpec) + (*in).DeepCopyInto(*out) + } + if in.ConfigSecret != nil { + in, out := &in.ConfigSecret, &out.ConfigSecret + *out = new(corev1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.MetricsPort != nil { + in, out := &in.MetricsPort, &out.MetricsPort + *out = new(int32) + **out = **in + } + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(int32) + **out = **in + } + if in.DomainName != nil { + in, out := &in.DomainName, &out.DomainName + *out = new(string) + **out = **in + } + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = new(IngressSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3GatewaySpec. +func (in *S3GatewaySpec) DeepCopy() *S3GatewaySpec { + if in == nil { + return nil + } + out := new(S3GatewaySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Seaweed) DeepCopyInto(out *Seaweed) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Seaweed. +func (in *Seaweed) DeepCopy() *Seaweed { + if in == nil { + return nil + } + out := new(Seaweed) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Seaweed) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeaweedList) DeepCopyInto(out *SeaweedList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Seaweed, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeaweedList. +func (in *SeaweedList) DeepCopy() *SeaweedList { + if in == nil { + return nil + } + out := new(SeaweedList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SeaweedList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeaweedSpec) DeepCopyInto(out *SeaweedSpec) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + (*in).DeepCopyInto(*out) + } + if in.Master != nil { + in, out := &in.Master, &out.Master + *out = new(MasterSpec) + (*in).DeepCopyInto(*out) + } + if in.Volume != nil { + in, out := &in.Volume, &out.Volume + *out = new(VolumeSpec) + (*in).DeepCopyInto(*out) + } + if in.VolumeTopology != nil { + in, out := &in.VolumeTopology, &out.VolumeTopology + *out = make(map[string]*VolumeTopologySpec, len(*in)) + for key, val := range *in { + var outVal *VolumeTopologySpec + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = new(VolumeTopologySpec) + (*in).DeepCopyInto(*out) + } + (*out)[key] = outVal + } + } + if in.Filer != nil { + in, out := &in.Filer, &out.Filer + *out = new(FilerSpec) + (*in).DeepCopyInto(*out) + } + if in.Admin != nil { + in, out := &in.Admin, &out.Admin + *out = new(AdminSpec) + (*in).DeepCopyInto(*out) + } + if in.Worker != nil { + in, out := &in.Worker, &out.Worker + *out = new(WorkerSpec) + (*in).DeepCopyInto(*out) + } + if in.S3 != nil { + in, out := &in.S3, &out.S3 + *out = new(S3GatewaySpec) + (*in).DeepCopyInto(*out) + } + if in.PVReclaimPolicy != nil { + in, out := &in.PVReclaimPolicy, &out.PVReclaimPolicy + *out = new(corev1.PersistentVolumeReclaimPolicy) + **out = **in + } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]corev1.LocalObjectReference, len(*in)) + copy(*out, *in) + } + if in.EnablePVReclaim != nil { + in, out := &in.EnablePVReclaim, &out.EnablePVReclaim + *out = new(bool) + **out = **in + } + if in.HostNetwork != nil { + in, out := &in.HostNetwork, &out.HostNetwork + *out = new(bool) + **out = **in + } + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(corev1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.VolumeServerDiskCount != nil { + in, out := &in.VolumeServerDiskCount, &out.VolumeServerDiskCount + *out = new(int32) + **out = **in + } + if in.HostSuffix != nil { + in, out := &in.HostSuffix, &out.HostSuffix + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeaweedSpec. +func (in *SeaweedSpec) DeepCopy() *SeaweedSpec { + if in == nil { + return nil + } + out := new(SeaweedSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeaweedStatus) DeepCopyInto(out *SeaweedStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.Master = in.Master + out.Volume = in.Volume + out.Filer = in.Filer + out.Admin = in.Admin + out.Worker = in.Worker + out.S3 = in.S3 +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeaweedStatus. +func (in *SeaweedStatus) DeepCopy() *SeaweedStatus { + if in == nil { + return nil + } + out := new(SeaweedStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.LoadBalancerIP != nil { + in, out := &in.LoadBalancerIP, &out.LoadBalancerIP + *out = new(string) + **out = **in + } + if in.ClusterIP != nil { + in, out := &in.ClusterIP, &out.ClusterIP + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceSpec. +func (in *ServiceSpec) DeepCopy() *ServiceSpec { + if in == nil { + return nil + } + out := new(ServiceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSIssuerRef) DeepCopyInto(out *TLSIssuerRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSIssuerRef. +func (in *TLSIssuerRef) DeepCopy() *TLSIssuerRef { + if in == nil { + return nil + } + out := new(TLSIssuerRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSSpec) DeepCopyInto(out *TLSSpec) { + *out = *in + if in.IssuerRef != nil { + in, out := &in.IssuerRef, &out.IssuerRef + *out = new(TLSIssuerRef) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSpec. +func (in *TLSSpec) DeepCopy() *TLSSpec { + if in == nil { + return nil + } + out := new(TLSSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VolumeServerConfig) DeepCopyInto(out *VolumeServerConfig) { + *out = *in + in.ComponentSpec.DeepCopyInto(&out.ComponentSpec) + in.ResourceRequirements.DeepCopyInto(&out.ResourceRequirements) + if in.Service != nil { + in, out := &in.Service, &out.Service + *out = new(ServiceSpec) + (*in).DeepCopyInto(*out) + } + if in.StorageClassName != nil { + in, out := &in.StorageClassName, &out.StorageClassName + *out = new(string) + **out = **in + } + if in.MetricsPort != nil { + in, out := &in.MetricsPort, &out.MetricsPort + *out = new(int32) + **out = **in + } + if in.CompactionMBps != nil { + in, out := &in.CompactionMBps, &out.CompactionMBps + *out = new(int32) + **out = **in + } + if in.FileSizeLimitMB != nil { + in, out := &in.FileSizeLimitMB, &out.FileSizeLimitMB + *out = new(int32) + **out = **in + } + if in.FixJpgOrientation != nil { + in, out := &in.FixJpgOrientation, &out.FixJpgOrientation + *out = new(bool) + **out = **in + } + if in.IdleTimeout != nil { + in, out := &in.IdleTimeout, &out.IdleTimeout + *out = new(int32) + **out = **in + } + if in.MaxVolumeCounts != nil { + in, out := &in.MaxVolumeCounts, &out.MaxVolumeCounts + *out = new(int32) + **out = **in + } + if in.MinFreeSpacePercent != nil { + in, out := &in.MinFreeSpacePercent, &out.MinFreeSpacePercent + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeServerConfig. +func (in *VolumeServerConfig) DeepCopy() *VolumeServerConfig { + if in == nil { + return nil + } + out := new(VolumeServerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VolumeSpec) DeepCopyInto(out *VolumeSpec) { + *out = *in + in.VolumeServerConfig.DeepCopyInto(&out.VolumeServerConfig) + if in.Rack != nil { + in, out := &in.Rack, &out.Rack + *out = new(string) + **out = **in + } + if in.DataCenter != nil { + in, out := &in.DataCenter, &out.DataCenter + *out = new(string) + **out = **in + } + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = new(IngressSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeSpec. +func (in *VolumeSpec) DeepCopy() *VolumeSpec { + if in == nil { + return nil + } + out := new(VolumeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VolumeTopologySpec) DeepCopyInto(out *VolumeTopologySpec) { + *out = *in + in.VolumeServerConfig.DeepCopyInto(&out.VolumeServerConfig) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeTopologySpec. +func (in *VolumeTopologySpec) DeepCopy() *VolumeTopologySpec { + if in == nil { + return nil + } + out := new(VolumeTopologySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkerSpec) DeepCopyInto(out *WorkerSpec) { + *out = *in + in.ComponentSpec.DeepCopyInto(&out.ComponentSpec) + in.ResourceRequirements.DeepCopyInto(&out.ResourceRequirements) + if in.MetricsPort != nil { + in, out := &in.MetricsPort, &out.MetricsPort + *out = new(int32) + **out = **in + } + if in.Persistence != nil { + in, out := &in.Persistence, &out.Persistence + *out = new(PersistenceSpec) + (*in).DeepCopyInto(*out) + } + if in.JobType != nil { + in, out := &in.JobType, &out.JobType + *out = new(string) + **out = **in + } + if in.MaxDetect != nil { + in, out := &in.MaxDetect, &out.MaxDetect + *out = new(int32) + **out = **in + } + if in.MaxExecute != nil { + in, out := &in.MaxExecute, &out.MaxExecute + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerSpec. +func (in *WorkerSpec) DeepCopy() *WorkerSpec { + if in == nil { + return nil + } + out := new(WorkerSpec) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/wandb/manifest/image_ref_test.go b/pkg/wandb/manifest/image_ref_test.go new file mode 100644 index 00000000..5e427b9f --- /dev/null +++ b/pkg/wandb/manifest/image_ref_test.go @@ -0,0 +1,88 @@ +package manifest_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + manifest "github.com/wandb/operator/pkg/wandb/manifest" +) + +var _ = Describe("ImageRef.GetImage", func() { + DescribeTable("resolves the image string", + func(img manifest.ImageRef, globalImageRegistry, expected string) { + Expect(img.GetImage(globalImageRegistry)).To(Equal(expected)) + }, + + // 1) No image from the manifest and no global registry. GetImage returns + // "", which is the signal for the spec layer to fall back to its own + // hardcoded default image. + Entry("no manifest image, no global registry", + manifest.ImageRef{}, "", ""), + + // 2) Manifest supplies the image and no global registry override: use the + // manifest image verbatim. + Entry("manifest image, no global registry", + manifest.ImageRef{Registry: "quay.io", Repository: "opstree/redis", Tag: "v7.0.15"}, + "", + "quay.io/opstree/redis:v7.0.15"), + + // 3) Manifest image has no registry component and the repository is a + // Docker Hub namespace (not a host). The global registry is prepended and + // the namespace is preserved. + Entry("manifest image without registry (dockerhub namespace), global registry set", + manifest.ImageRef{Repository: "opstree/redis", Tag: "v7.0.15"}, + "myregistry.io", + "myregistry.io/opstree/redis:v7.0.15"), + + // 4) Manifest image has a registry and a global registry is set: the global + // registry is prepended in front of the manifest's registry (mirror-style), + // it does not replace it. + Entry("manifest image with registry, global registry set", + manifest.ImageRef{Registry: "quay.io", Repository: "opstree/redis", Tag: "v7.0.15"}, + "myregistry.io", + "myregistry.io/quay.io/opstree/redis:v7.0.15"), + + // --- Old-manifest shapes: registry embedded in Repository, Registry unset --- + + // 5) Registry embedded in Repository with a separate Tag, no override: + // the tag must be preserved (regression for the dropped-tag bug). + Entry("embedded registry in repository, separate tag, no override", + manifest.ImageRef{Repository: "quay.io/opstree/redis", Tag: "v7.0.15"}, + "", + "quay.io/opstree/redis:v7.0.15"), + + // 6) Whole image baked into Repository (tag included), no override: + // returned verbatim. + Entry("full image baked into repository, no override", + manifest.ImageRef{Repository: "quay.io/opstree/redis:v7.0.15"}, + "", + "quay.io/opstree/redis:v7.0.15"), + + // 7) Single-segment repository with a separate tag, no override: the tag + // must be preserved (regression for the dropped-tag bug). + Entry("single-segment repository, separate tag, no override", + manifest.ImageRef{Repository: "redis", Tag: "v7.0.15"}, + "", + "redis:v7.0.15"), + + // 8) Embedded registry host in Repository with an override: the whole + // repository (host included) is kept and the global registry is prepended. + Entry("embedded registry host in repository, global registry set", + manifest.ImageRef{Repository: "quay.io/opstree/redis", Tag: "v7.0.15"}, + "myregistry.io", + "myregistry.io/quay.io/opstree/redis:v7.0.15"), + + // 9) Port-qualified registry host embedded in Repository with an override: + // kept verbatim with the global registry prepended. + Entry("port-qualified registry host in repository, global registry set", + manifest.ImageRef{Repository: "localhost:5000/opstree/redis", Tag: "v7.0.15"}, + "myregistry.io", + "myregistry.io/localhost:5000/opstree/redis:v7.0.15"), + + // 10) Digest is preserved and takes precedence over tag. + Entry("manifest image with digest, no override", + manifest.ImageRef{Registry: "quay.io", Repository: "opstree/redis", Digest: "sha256:abc123"}, + "", + "quay.io/opstree/redis@sha256:abc123"), + ) +}) diff --git a/pkg/wandb/manifest/load_manifest_from_files_test.go b/pkg/wandb/manifest/load_manifest_from_files_test.go new file mode 100644 index 00000000..18b2b4c1 --- /dev/null +++ b/pkg/wandb/manifest/load_manifest_from_files_test.go @@ -0,0 +1,97 @@ +package manifest_test + +import ( + "context" + "fmt" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + manifest "github.com/wandb/operator/pkg/wandb/manifest" +) + +var _ = Describe("LoadManifestFromFiles", func() { + var ( + ctx context.Context + ) + + BeforeEach(func() { + ctx = context.Background() + }) + + Context("when loading multi-file manifests", func() { + It("should correctly merge manifest.yaml and sizing.yaml files", func() { + // Simple test to see if we get here + Expect(true).To(BeTrue()) + + // Get the path to the test manifests + manifestRoot, err := filepath.Abs("../../../hack/testing-manifests/server-manifest") + Expect(err).NotTo(HaveOccurred()) + repository := "file://" + filepath.ToSlash(manifestRoot) + + // Load the manifest using the public API which will internally call loadManifestFromFiles + m, err := manifest.LoadManifestFromFile(ctx, repository, "0.83.0-clickhouse-keeper.2") + Expect(err).NotTo(HaveOccurred()) + Expect(m).NotTo(BeNil()) + + // Test basic properties from manifest.yaml + Expect(m.RequiredOperatorVersion).To(Equal("^2.0.0")) + Expect(m.Features["filestreamQueue"]).To(BeFalse()) + Expect(m.Features["proxy"]).To(BeFalse()) + + // Test Kafka topics + Expect(m.Kafka.Topics).To(HaveLen(4)) + Expect(m.Kafka.Topics[0].Name).To(Equal("filestream")) + Expect(m.Kafka.Topics[0].Topic).To(Equal("filestream")) + + Expect(m.Bucket["default"].Sizing).NotTo(BeNil()) + Expect(m.Bucket["default"].Ingress).NotTo(BeNil()) + + // Debug output + fmt.Printf("Bucket default ingress: %+v\n", m.Bucket["default"].Ingress) + fmt.Printf("Bucket default sizing: %+v\n", m.Bucket["default"].Sizing) + + // Test sizing configurations that come from sizing.yaml + // Check that Kafka sizing was loaded + Expect(m.Kafka.Sizing).NotTo(BeEmpty()) + Expect(m.Kafka.Sizing["default"].Replicas).To(Equal(int32(2))) + Expect(m.Kafka.Sizing["micro"].Replicas).To(Equal(int32(2))) + + // Check that Bucket sizing was loaded + Expect(m.Bucket["default"].Sizing["default"].Replicas).To(Equal(int32(1))) + Expect(m.Bucket["default"].Sizing["micro"].Replicas).To(Equal(int32(3))) + + // Check that MySQL sizing was loaded + Expect(m.Mysql["default"].Sizing["default"].Replicas).To(Equal(int32(1))) + Expect(m.Mysql["default"].Sizing["micro"].Replicas).To(Equal(int32(3))) + + // Check that Redis sizing was loaded + Expect(m.Redis["default"].Sizing["default"].Replicas).To(Equal(int32(1))) + Expect(m.Redis["default"].Sizing["micro"].Replicas).To(Equal(int32(2))) + + // Check that Clickhouse sizing was loaded + Expect(m.Clickhouse["default"].Sizing["default"].Shards).To(Equal(int32(1))) + Expect(m.Clickhouse["default"].Sizing["default"].Replicas).To(Equal(int32(1))) + }) + }) + + Context("when testing file loading order", func() { + It("verifies the existing test covers multi-file loading", func() { + // This test verifies that the existing test in manifest_decode_test.go + // actually exercises the loadManifestFromFiles function with multiple files + + manifestRoot, err := filepath.Abs("../../../hack/testing-manifests/server-manifest") + Expect(err).NotTo(HaveOccurred()) + repository := "file://" + filepath.ToSlash(manifestRoot) + + // This will internally call loadManifestFromFiles with multiple files + m, err := manifest.LoadManifestFromFile(ctx, repository, "0.83.0-clickhouse-keeper.2") + Expect(err).NotTo(HaveOccurred()) + + // Confirm we have merged data from both files + Expect(m.RequiredOperatorVersion).To(Equal("^2.0.0")) // From manifest.yaml + Expect(m.Kafka.Sizing["default"].Replicas).To(Equal(int32(2))) // From sizing.yaml + }) + }) +}) diff --git a/pkg/wandb/manifest/manifest.go b/pkg/wandb/manifest/manifest.go new file mode 100644 index 00000000..d25fdb25 --- /dev/null +++ b/pkg/wandb/manifest/manifest.go @@ -0,0 +1,846 @@ +package manifest + +import ( + "archive/tar" + "compress/gzip" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "maps" + "net/url" + "os" + "path" + "path/filepath" + "slices" + "strconv" + "strings" + + "log/slog" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + v2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/logx" + autoscalingv2 "k8s.io/api/autoscaling/v2" + + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/content/oci" + "oras.land/oras-go/v2/registry/remote" + //"oras.land/oras-go/v2/registry/remote/retry" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +// Manifest defines the structure of the server manifest YAML (e.g. 0.76.1.yaml). +// It is intended to be a direct mapping of the YAML document for decoding via +// gopkg.in/yaml.v3 or sigs.k8s.io/yaml. +type Manifest struct { + RequiredOperatorVersion string `yaml:"requiredOperatorVersion"` + Features map[string]bool `yaml:"features,omitempty"` + // Prefer plural, but accept singular key as found in some manifests. + GeneratedSecrets []GeneratedSecret `yaml:"generatedSecrets,omitempty"` + // CommonEnvvars defines reusable groups of env vars that can be referenced + // by applications via the per-application `commonEnvs` list. This maps a + // group name (e.g., "gorillaMysql") to a slice of EnvVar definitions. + CommonEnvvars map[string][]EnvVar `yaml:"commonEnvvars,omitempty"` + CommonVolumeMounts map[string][]VolumeMount `yaml:"commonVolumeMounts,omitempty"` + Bucket map[string]InfraConfig `yaml:"bucket"` + Clickhouse map[string]InfraConfig `yaml:"clickhouse"` + ClickhouseKeeper map[string]InfraConfig `yaml:"clickhouseKeeper"` + Kafka KafkaConfig `yaml:"kafka"` + Mysql map[string]InfraConfig `yaml:"mysql"` + Redis map[string]InfraConfig `yaml:"redis"` + Applications map[string]Application `yaml:"applications"` + // Migrations captures per-database migration jobs (e.g., default, runsdb, usagedb) + // as found in 0.76.1.yaml under the top-level "migrations" key. + Migrations map[string]MigrationJob `yaml:"migrations,omitempty"` +} + +// GeneratedSecret represents the configuration for a dynamically generated secret. +type GeneratedSecret struct { + Name string `yaml:"name"` + Length int `yaml:"length"` + CharacterType string `yaml:"type"` + // UseExactName when true, creates the secret with the exact name specified without prefixing it with the CR name. + // This is useful for secrets that need to be referenced by external systems with a fixed name. + UseExactName bool `yaml:"useExactName,omitempty"` +} + +type InfraConfig struct { + Sizing map[v2.Size]SizingConfig `yaml:"sizing"` + Ingress *AppIngressSpec `yaml:"ingress,omitempty"` + Images map[string]ImageRef `yaml:"images,omitempty"` +} + +// KafkaTopicDef models a topic configuration used both at the top-level +// kafka section and inside per-application kafka sections. +type KafkaTopicDef struct { + Topic string `yaml:"topic"` + PartitionCount int `yaml:"partitionCount,omitempty"` + ConsumerGroup string `yaml:"consumerGroup,omitempty"` +} + +// KafkaConfig represents the top-level kafka section with sizing and topics. +type KafkaConfig struct { + Sizing map[v2.Size]KafkaSizingConfig `yaml:"sizing"` + Topics []KafkaTopic `yaml:"topics"` + // Images holds every image the managed Kafka deployment pulls, keyed by + // component (e.g. "bufstream", "etcd", "bucketEnsure"). Keeping them all in + // one map lets a manifest mirror every image a managed Kafka install needs. + Images map[string]ImageRef `yaml:"images,omitempty"` +} + +// KafkaTopic models one entry in the kafka topics list in the YAML. +type KafkaTopic struct { + Name string `yaml:"name"` + Features []string `yaml:"features,omitempty"` + Topic string `yaml:"topic"` + PartitionCount int `yaml:"partitionCount,omitempty"` +} + +// ImageRef represents an application container image reference. +type ImageRef struct { + Registry string `yaml:"registry"` + Repository string `yaml:"repository"` + Tag string `yaml:"tag,omitempty"` + Digest string `yaml:"digest,omitempty"` +} + +func (img ImageRef) GetImage(registry string) string { + reg := img.Registry // from manifest + repository := img.Repository // from manifest, older verisons of manifest will contain both registry and repository in this field + + // manifest's ImageRef.Registry is blank, check if registry exists as part of repository string + var wandbImage string + if reg == "" { + wandbImage = repository + } else { // manifest's ImageRef.Registry exists. Combine with repo for full wandb path + wandbImage = reg + "/" + repository + } + + // user provided global image registry, prepend full wandb reg.repo path + image := wandbImage + if registry != "" { + image = registry + "/" + wandbImage + } + + switch { + case img.Digest != "": + return image + "@" + img.Digest + case img.Tag != "": + return image + ":" + img.Tag + default: + return image + } +} + +// AppKafkaSection is the per-application kafka section; fields are optional +// and mirror the top-level topics. +type AppKafkaSection struct { + Sizing map[v2.Size]SizingConfig `yaml:"sizing"` + Topics map[string]KafkaTopicDef `yaml:"topics"` +} + +// Application describes one entry in the applications list. +type Application struct { + Name string `yaml:"name"` + // LegacyKey is the v1 operator-wandb helm values key for this application + // when it differs from the name (e.g. nginx-proxy was `nginx`). + LegacyKey string `yaml:"legacyKey,omitempty"` + Image ImageRef `yaml:"image"` + Args []string `yaml:"args,omitempty"` + Command []string `yaml:"command,omitempty"` + // CommonEnvs is a list of keys referencing top-level commonEnvvars groups + // to be included for this application (e.g., ["gorillaMysql", "gorillaBucket"]). + CommonEnvs []string `yaml:"commonEnvs,omitempty"` + CommonVolumeMounts []string `yaml:"commonVolumeMounts,omitempty"` + // InitContainers allows specifying per-application init containers + InitContainers []ContainerSpec `yaml:"initContainers,omitempty"` + // Containers allows specifying per-application containers for multi-container apps + Containers []ContainerSpec `yaml:"containers,omitempty"` + // Features enables this application only when specific feature flags are set in the + // top-level manifest features. In the YAML this appears as a list of strings. + Features []string `yaml:"features,omitempty"` + Env []EnvVar `yaml:"env,omitempty"` + Service *ServiceSpec `yaml:"service,omitempty"` + // Files allows injecting files into the application's container by mounting + // data from ConfigMaps. Each entry may either inline file contents (stored + // into an operator-managed ConfigMap) or reference an existing ConfigMap. + // The file will be mounted at the provided mountPath/FileName using subPath. + Files []FileSpec `yaml:"files,omitempty"` + JWTTokens []JWTToken `yaml:"jwtTokens,omitempty"` + VolumeMounts []VolumeMount `yaml:"volumeMounts,omitempty"` + Sizing map[v2.Size]SizingConfig `yaml:"sizing,omitempty"` + Ingress *AppIngressSpec `yaml:"ingress,omitempty"` +} + +type AppIngressSpec struct { + Paths []string `yaml:"paths,omitempty"` + ServicePort string `yaml:"servicePort,omitempty"` + PathType string `yaml:"pathType,omitempty"` +} + +type SizingConfig struct { + Replicas int32 `yaml:"replicas,omitempty"` + Shards int32 `yaml:"shards,omitempty"` + Copies int32 `yaml:"copies,omitempty"` + VolumeSize string `yaml:"volumeSize,omitempty"` + Resources *corev1.ResourceRequirements `yaml:"resources,omitempty"` + Autoscaling *AutoscalingConfig `yaml:"autoscaling,omitempty"` + // MetadataVolumeSize sizes the disk backing an object store's index/metadata + MetadataVolumeSize string `yaml:"metadataVolumeSize,omitempty"` +} + +type KafkaSizingConfig struct { + SizingConfig `yaml:",inline"` + ReplicationFactor int32 `yaml:"replicationFactor,omitempty"` + MinInSyncReplicas int32 `yaml:"minInSyncReplicas,omitempty"` + OffsetsTopicRF int32 `yaml:"offsetsTopicRF,omitempty"` + TransactionStateRF int32 `yaml:"transactionStateRF,omitempty"` + TransactionStateISR int32 `yaml:"transactionStateISR,omitempty"` +} + +type AutoscalingConfig struct { + Horizontal autoscalingv2.HorizontalPodAutoscalerSpec +} + +// ContainerSpec represents a minimal container definition used by +// application-level initContainers entries in the manifest. +type ContainerSpec struct { + Name string `yaml:"name"` + Image ImageRef `yaml:"image"` + Args []string `yaml:"args,omitempty"` + Command []string `yaml:"command,omitempty"` + Ports []ContainerPort `yaml:"ports,omitempty"` + LivenessProbe *corev1.Probe `yaml:"livenessProbe,omitempty"` + ReadinessProbe *corev1.Probe `yaml:"readinessProbe,omitempty"` + StartupProbe *corev1.Probe `yaml:"startupProbe,omitempty"` + Resources *corev1.ResourceRequirements +} + +// EnvVar models an application environment variable sourced from manifest-defined services. +type EnvVar struct { + Name string `yaml:"name"` + Value string `yaml:"value,omitempty"` + ValueFrom *corev1.EnvVarSource `yaml:"valueFrom,omitempty"` + Sources []EnvSource `yaml:"sources,omitempty"` + DefaultValue string `yaml:"defaultValue,omitempty"` +} + +type VolumeMount struct { + MountPath string `yaml:"mountPath"` + Name string `yaml:"name"` + Source EnvSource `yaml:"source"` +} + +// EnvSource references a named source and its type (e.g., moco, redis, bucket). +type EnvSource struct { + Name string `yaml:"name"` + Type string `yaml:"type"` + Field string `yaml:"field,omitempty"` + Proto string `yaml:"proto,omitempty"` + Path string `yaml:"path,omitempty"` + Port string `yaml:"port,omitempty"` +} + +// ServiceSpec represents an optional Service definition for an application +// (currently only ports are modeled as per 0.76.1.yaml needs). +type ServiceSpec struct { + Type corev1.ServiceType `yaml:"type,omitempty"` + Ports []corev1.ServicePort `yaml:"ports,omitempty"` +} + +// ContainerPort models a single port entry in an application's ports section. +type ContainerPort struct { + ContainerPort int32 `yaml:"containerPort"` + Protocol corev1.Protocol `yaml:"protocol,omitempty"` + Name string `yaml:"name,omitempty"` +} + +// MigrationJob represents a migration invocation with an image and args, used +// by the top-level "migrations" section (e.g., default, runsdb, usagedb). +type MigrationJob struct { + Image ImageRef `yaml:"image"` + Args []string `yaml:"args,omitempty"` + Command []string `yaml:"command,omitempty"` + CommonEnvs []string `yaml:"commonEnvs,omitempty"` + CommonVolumeMounts []string `yaml:"commonVolumeMounts,omitempty"` + Env []EnvVar `yaml:"env,omitempty"` + VolumeMounts []VolumeMount `yaml:"volumeMounts,omitempty"` +} + +// FileSpec defines a single file to project into the application's container. +// Exactly one of Inline or ConfigMapRef should be provided. The file is mounted +// as a single file using subPath. MountPath should be a directory that already +// exists in the container image (e.g., /etc/nginx/conf.d). If FileName is not +// provided, Name will be used as the target filename. Name is also the key name +// stored inside the ConfigMap data. +type FileSpec struct { + // Name is the key used in the ConfigMap data and defaults to the filename + // if FileName is not provided. + Name string `yaml:"name"` + // MountPath is the directory inside the container where the file should be placed. + MountPath string `yaml:"mountPath"` + // FileName is the filename to write within MountPath. Optional; defaults to Name. + FileName string `yaml:"fileName,omitempty"` + // Inline is the file contents to embed directly into an operator-managed ConfigMap. + Inline string `yaml:"inline,omitempty"` + // ConfigMapRef references an existing ConfigMap (in the same namespace) to source the file from. + // When set, Inline should be empty. + ConfigMapRef string `yaml:"configMapRef,omitempty"` +} + +func GetServerManifest(ctx context.Context, repository string, version string) (Manifest, error) { + versionURL, err := url.Parse(repository) + if err != nil { + return Manifest{}, err + } + + switch versionURL.Scheme { + case "http", "https": + return Manifest{}, errors.New("http manifest not implemented") + case "file": + return LoadManifestFromFile(ctx, repository, version) + default: + return DownloadServerManifest(ctx, repository, version) + } +} + +func LoadManifestFromFile(ctx context.Context, repository string, version string) (Manifest, error) { + repositoryURL, err := url.Parse(repository) + if err != nil { + return Manifest{}, err + } + basePath := repositoryURL.Path + + rootManifestFile := path.Join(basePath, fmt.Sprintf("%s.yaml", version)) + if rootManifestFileInfo, err := os.Stat(rootManifestFile); err == nil && !rootManifestFileInfo.IsDir() { + return loadManifestFromFiles(ctx, []string{rootManifestFile}) + } + + versionDir := filepath.Join(basePath, version) + versionDirInfo, err := os.Stat(versionDir) + if err != nil || !versionDirInfo.IsDir() { + if err == nil { + return Manifest{}, fmt.Errorf("manifest path %q is not a directory", versionDir) + } + return Manifest{}, fmt.Errorf( + "failed to locate manifest for version %q in %q: %w", + version, + basePath, + err, + ) + } + + manifestFiles, err := filepath.Glob(filepath.Join(versionDir, "*.yaml")) + if err != nil { + return Manifest{}, err + } + if len(manifestFiles) == 0 { + return Manifest{}, fmt.Errorf("no manifest files found in %q", versionDir) + } + slices.Sort(manifestFiles) + + return loadManifestFromFiles(ctx, manifestFiles) +} + +func loadManifestFromFiles(ctx context.Context, manifestFiles []string) (Manifest, error) { + logger := logx.GetSlog(ctx) + manifest := Manifest{} + + for _, manifestFile := range manifestFiles { + manifestData, err := os.ReadFile(manifestFile) + if err != nil { + logger.Error("failed to read manifest file", "file", manifestFile, "error", err) + return Manifest{}, err + } + var fileManifest Manifest + if err = yaml.Unmarshal(manifestData, &fileManifest); err != nil { + logger.Error("failed to unmarshal manifest file", "file", manifestFile, "error", err) + return Manifest{}, fmt.Errorf("failed to unmarshal %q: %w", manifestFile, err) + } + + // Simple merge: preserve existing data, add new data + mergeSimple(&manifest, &fileManifest) + } + + logger.Debug("loaded manifest files", "count", len(manifestFiles), "files", manifestFiles, "manifest", manifest) + return manifest, nil +} + +// mergeSimple performs a simple merge that preserves existing data and adds new data +func mergeSimple(dst, src *Manifest) { + // Preserve existing data, add new data from src + + // Basic fields - only set if dst is zero + if dst.RequiredOperatorVersion == "" { + dst.RequiredOperatorVersion = src.RequiredOperatorVersion + } + if dst.Features == nil { + dst.Features = src.Features + } else if src.Features != nil { + for k, v := range src.Features { + dst.Features[k] = v + } + } + + // Infrastructure configs - merge maps + if src.Bucket != nil { + if dst.Bucket == nil { + dst.Bucket = make(map[string]InfraConfig) + } + mergeInfraConfigs(dst.Bucket, src.Bucket) + } + if src.Mysql != nil { + if dst.Mysql == nil { + dst.Mysql = make(map[string]InfraConfig) + } + mergeInfraConfigs(dst.Mysql, src.Mysql) + } + if src.Redis != nil { + if dst.Redis == nil { + dst.Redis = make(map[string]InfraConfig) + } + mergeInfraConfigs(dst.Redis, src.Redis) + } + if src.Clickhouse != nil { + if dst.Clickhouse == nil { + dst.Clickhouse = make(map[string]InfraConfig) + } + mergeInfraConfigs(dst.Clickhouse, src.Clickhouse) + } + if src.ClickhouseKeeper != nil { + if dst.ClickhouseKeeper == nil { + dst.ClickhouseKeeper = make(map[string]InfraConfig) + } + mergeInfraConfigs(dst.ClickhouseKeeper, src.ClickhouseKeeper) + } + + // Kafka sizing + if src.Kafka.Sizing != nil { + if dst.Kafka.Sizing == nil { + dst.Kafka.Sizing = make(map[v2.Size]KafkaSizingConfig) + } + for k, v := range src.Kafka.Sizing { + dst.Kafka.Sizing[k] = v + } + } + + // Kafka images - the Images map can live in a different manifest file than + // the kafka sizing/topics, so merge it explicitly rather than letting it get + // dropped. + if src.Kafka.Images != nil { + if dst.Kafka.Images == nil { + dst.Kafka.Images = make(map[string]ImageRef) + } + for k, v := range src.Kafka.Images { + dst.Kafka.Images[k] = v + } + } + + // Applications - merge maps + if src.Applications != nil { + if dst.Applications == nil { + dst.Applications = make(map[string]Application) + } + mergeApplications(dst.Applications, src.Applications) + } + + // Migrations + if src.Migrations != nil { + if dst.Migrations == nil { + dst.Migrations = make(map[string]MigrationJob) + } + for k, v := range src.Migrations { + dst.Migrations[k] = v + } + } + + // GeneratedSecrets - only from first file + if len(dst.GeneratedSecrets) == 0 { + dst.GeneratedSecrets = src.GeneratedSecrets + } + + // CommonEnvvars - merge maps + if src.CommonEnvvars != nil { + if dst.CommonEnvvars == nil { + dst.CommonEnvvars = make(map[string][]EnvVar) + } + for k, v := range src.CommonEnvvars { + dst.CommonEnvvars[k] = v + } + } + + // CommonVolumeMounts - merge maps + if src.CommonVolumeMounts != nil { + if dst.CommonVolumeMounts == nil { + dst.CommonVolumeMounts = make(map[string][]VolumeMount) + } + for k, v := range src.CommonVolumeMounts { + dst.CommonVolumeMounts[k] = v + } + } + + // Kafka topics - only from first file (they don't exist in sizing.yaml anyway) + if len(dst.Kafka.Topics) == 0 { + dst.Kafka.Topics = src.Kafka.Topics + } +} + +// mergeInfraConfigs merges two map[string]InfraConfig maps +func mergeInfraConfigs(dst, src map[string]InfraConfig) { + if src == nil { + return + } + if dst == nil { + return + } + + for name, srcConfig := range src { + if dstConfig, exists := dst[name]; exists { + // Merge existing config + mergedConfig := dstConfig + + // Preserve ingress from dst if src doesn't have it + if srcConfig.Ingress == nil && dstConfig.Ingress != nil { + mergedConfig.Ingress = dstConfig.Ingress + } else if srcConfig.Ingress != nil { + mergedConfig.Ingress = srcConfig.Ingress + } + + // Merge sizing maps + if srcConfig.Sizing != nil { + if mergedConfig.Sizing == nil { + mergedConfig.Sizing = make(map[v2.Size]SizingConfig) + } + for k, v := range srcConfig.Sizing { + mergedConfig.Sizing[k] = v + } + } + + dst[name] = mergedConfig + } else { + // New config + dst[name] = srcConfig + } + } +} + +// mergeApplications merges two map[string]Application maps +func mergeApplications(dst, src map[string]Application) { + if src == nil { + return + } + if dst == nil { + return + } + + for name, srcApp := range src { + if dstApp, exists := dst[name]; exists { + // Merge existing application + mergedApp := dstApp + + // Preserve ingress from dst if src doesn't have it + if srcApp.Ingress == nil && dstApp.Ingress != nil { + mergedApp.Ingress = dstApp.Ingress + } else if srcApp.Ingress != nil { + mergedApp.Ingress = srcApp.Ingress + } + + // Merge sizing maps + if srcApp.Sizing != nil { + if mergedApp.Sizing == nil { + mergedApp.Sizing = make(map[v2.Size]SizingConfig) + } + for k, v := range srcApp.Sizing { + mergedApp.Sizing[k] = v + } + } + + // Merge common envs (avoid duplicates) + if len(srcApp.CommonEnvs) > 0 { + envSet := make(map[string]bool) + for _, env := range mergedApp.CommonEnvs { + envSet[env] = true + } + for _, env := range srcApp.CommonEnvs { + if !envSet[env] { + mergedApp.CommonEnvs = append(mergedApp.CommonEnvs, env) + } + } + } + + dst[name] = mergedApp + } else { + // New application + dst[name] = srcApp + } + } +} + +func DownloadServerManifest(ctx context.Context, repository string, version string) (Manifest, error) { + logger := logx.GetSlog(ctx) + var manifest Manifest + + repository = strings.TrimPrefix(repository, "oci://") + + ociDir := "/tmp/server-manifest" + localRepo, err := oci.New(ociDir) + if err != nil { + return manifest, err + } + + var descriptor ocispec.Descriptor + descriptor, err = localRepo.Resolve(ctx, version) + if err != nil { + logger.Info("image not found in local", "repository", repository, "version", version, "error", err) + remoteRepo, err := remote.NewRepository(repository) + if err != nil { + logger.Error("failed to create repository", "repository", repository, "version", version, "error", err) + return manifest, err + } + descriptor, err = oras.Copy(ctx, remoteRepo, version, localRepo, version, oras.DefaultCopyOptions) + if err != nil { + logger.Error("failed to fetch image from remote", "repository", repository, "version", version, "error", err) + return manifest, err + } + logger.Info("successfully fetched image from remote", "repository", repository, "version", version, "desc", descriptor) + } else { + logger.Debug("successfully fetched image from local", "repository", repository, "version", version) + } + + return processManifest(ctx, localRepo, descriptor, logger) +} + +func processManifest(ctx context.Context, repo oras.ReadOnlyTarget, descriptor ocispec.Descriptor, logger *slog.Logger) (Manifest, error) { + var manifest Manifest + + switch descriptor.MediaType { + case ocispec.MediaTypeImageIndex, "application/vnd.docker.distribution.manifest.list.v2+json": + indexData, err := content.FetchAll(ctx, repo, descriptor) + if err != nil { + return manifest, fmt.Errorf("failed to fetch index: %w", err) + } + var index ocispec.Index + if err := json.Unmarshal(indexData, &index); err != nil { + return manifest, fmt.Errorf("failed to unmarshal index: %w", err) + } + if len(index.Manifests) == 0 { + return manifest, errors.New("index has no manifests") + } + // For now, we just pick the first manifest. In the future we might want to match platform. + return processManifest(ctx, repo, index.Manifests[0], logger) + + case ocispec.MediaTypeImageManifest, "application/vnd.docker.distribution.manifest.v2+json": + manifestData, err := content.FetchAll(ctx, repo, descriptor) + if err != nil { + return manifest, fmt.Errorf("failed to fetch manifest: %w", err) + } + + var ociManifest ocispec.Manifest + if err := json.Unmarshal(manifestData, &ociManifest); err != nil { + return manifest, fmt.Errorf("failed to unmarshal manifest: %w", err) + } + + manifestFileContents := map[string][]byte{} + + for _, layer := range ociManifest.Layers { + layerReader, err := repo.Fetch(ctx, layer) + if err != nil { + logger.Error("failed to fetch layer", "digest", layer.Digest, "err", err) + continue + } + + var tr *tar.Reader + if layer.MediaType == ocispec.MediaTypeImageLayerGzip || layer.MediaType == "application/vnd.docker.image.rootfs.diff.tar.gzip" { + gzr, err := gzip.NewReader(layerReader) + if err != nil { + layerReader.Close() + logger.Error("failed to create gzip reader for layer", "digest", layer.Digest, "err", err) + continue + } + tr = tar.NewReader(gzr) + defer gzr.Close() + } else { + tr = tar.NewReader(layerReader) + } + defer layerReader.Close() + + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + logger.Error("failed to read tar header", "err", err) + break + } + + if filepath.Ext(header.Name) != ".yaml" { + continue + } + + manifestBytes, err := io.ReadAll(tr) + if err != nil { + logger.Error("failed to read manifest file", "file", header.Name, "err", err) + continue + } + + manifestFileContents[header.Name] = manifestBytes + } + } + + if len(manifestFileContents) == 0 { + return manifest, errors.New("no manifest yaml files found in image layers") + } + + manifestFiles := slices.Sorted(maps.Keys(manifestFileContents)) + for _, manifestFile := range manifestFiles { + var fileManifest Manifest + if err := yaml.Unmarshal(manifestFileContents[manifestFile], &fileManifest); err != nil { + logger.Error("failed to unmarshal manifest file", "file", manifestFile, "err", err) + return Manifest{}, fmt.Errorf("failed to unmarshal %q: %w", manifestFile, err) + } + + mergeSimple(&manifest, &fileManifest) + } + + logger.Debug("successfully unmarshaled manifest files", "files", manifestFiles, "manifest", manifest) + return manifest, nil + default: + return manifest, fmt.Errorf("unsupported media type: %s", descriptor.MediaType) + } +} + +// JWTToken defines a JWT token to be mounted into the application's container. +// This abstraction supports multiple token sources: Kubernetes service account tokens, +// pre-created secrets, or cloud provider token stores (CSI). +type JWTToken struct { + // Name is a unique identifier for this JWT token mount. + Name string `yaml:"name"` + // MountPath is the directory path where the token file will be mounted. + MountPath string `yaml:"mountPath"` + // Source specifies where the JWT token comes from. Exactly one source type should be set. + Source JWTTokenSource `yaml:"source"` +} + +// JWTTokenSource is a union type representing the different ways to source a JWT token. +// Exactly one field should be set. +type JWTTokenSource struct { + // KubernetesServiceAccount requests a token from the Kubernetes API server + // for the pod's service account with custom audience and expiration. + KubernetesServiceAccount *K8sServiceAccountToken `yaml:"kubernetesServiceAccount,omitempty"` + // SecretRef references an existing Kubernetes Secret containing the JWT token. + SecretRef *SecretReference `yaml:"secretRef,omitempty"` + // CSIProvider configures a CSI driver to fetch the token (e.g., AWS Secrets Manager, + // Azure Key Vault, GCP Secret Manager). + CSIProvider *CSIProviderConfig `yaml:"csiProvider,omitempty"` +} + +// K8sServiceAccountToken configures a Kubernetes service account token projection. +type K8sServiceAccountToken struct { + // Audience is the intended audience of the token (e.g., "internal-service"). + Audience string `yaml:"audience"` + // ExpirationSeconds is the token's lifetime. Kubernetes will auto-rotate before expiration. + // Optional; defaults to 3607 seconds (1 hour). + ExpirationSeconds int64 `yaml:"expirationSeconds,omitempty"` +} + +// SecretReference points to a Kubernetes Secret containing a JWT token. +type SecretReference struct { + // Name is the name of the Secret in the same namespace. + Name string `yaml:"name"` + // Key is the data key within the Secret that contains the token. + // Optional; defaults to "token" if not specified. + Key string `yaml:"key,omitempty"` +} + +// CSIProviderConfig configures a Container Storage Interface (CSI) driver +// for fetching JWT tokens from cloud provider secret stores. +type CSIProviderConfig struct { + // Driver is the CSI driver name (e.g., "secrets-store.csi.k8s.io"). + Driver string `yaml:"driver"` + // Parameters are driver-specific configuration key-value pairs. + Parameters map[string]string `yaml:"parameters,omitempty"` +} + +// FeaturesEnabled returns true if any of the supplied feature flags are +// enabled in the manifest's top-level Features section. +func (m *Manifest) FeaturesEnabled(topicFeatures []string) bool { + if len(topicFeatures) == 0 || m.Features == nil { + return false + } + for _, f := range topicFeatures { + if enabled, ok := m.Features[f]; ok && enabled { + return true + } + } + return false +} + +func (m *Manifest) ResolveServiceURL(src EnvSource) (string, bool) { + if src.Name == "" { + return "", false + } + + app, ok := m.Applications[src.Name] + if !ok { + return "", false + } + + port, ok := app.ResolveServicePortFromManifest(src.Port) + if !ok { + return "", false + } + + protoPrefix := "" + if src.Proto != "" { + protoPrefix = fmt.Sprintf("%s://", src.Proto) + } + return fmt.Sprintf("%s%s:%d%s", protoPrefix, src.Name, port, src.Path), true +} + +func (a *Application) ResolveServicePortFromManifest(requestedPort string) (int32, bool) { + if requestedPort != "" { + if n, err := strconv.ParseInt(requestedPort, 10, 32); err == nil { + return int32(n), true + } + } + + if a.Service != nil { + if requestedPort == "" && len(a.Service.Ports) > 0 { + return a.Service.Ports[0].Port, true + } + for _, p := range a.Service.Ports { + if p.Name == requestedPort { + return p.Port, true + } + } + } + + for _, container := range a.Containers { + if requestedPort == "" && len(container.Ports) > 0 { + return container.Ports[0].ContainerPort, true + } + for _, p := range container.Ports { + if p.Name == requestedPort { + return p.ContainerPort, true + } + } + } + for _, container := range a.InitContainers { + if requestedPort == "" && len(container.Ports) > 0 { + return container.Ports[0].ContainerPort, true + } + for _, p := range container.Ports { + if p.Name == requestedPort { + return p.ContainerPort, true + } + } + } + + return 0, false +} diff --git a/pkg/wandb/manifest/manifest_decode_test.go b/pkg/wandb/manifest/manifest_decode_test.go new file mode 100644 index 00000000..7d902df5 --- /dev/null +++ b/pkg/wandb/manifest/manifest_decode_test.go @@ -0,0 +1,53 @@ +package manifest_test + +import ( + "context" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + manifest "github.com/wandb/operator/pkg/wandb/manifest" +) + +var _ = Describe("Server manifest YAML decode", func() { + manifestRepository := func() string { + manifestRoot, err := filepath.Abs("../../../hack/testing-manifests/server-manifest") + Expect(err).NotTo(HaveOccurred()) + return "file://" + filepath.ToSlash(manifestRoot) + } + + It("merges multiple manifest files from a version directory", func() { + m, err := manifest.LoadManifestFromFile(context.Background(), manifestRepository(), "0.83.0-clickhouse-keeper.2") + Expect(err).NotTo(HaveOccurred()) + + // Features (match current testing manifest values) + Expect(m.Features["filestreamQueue"]).To(BeFalse()) + Expect(m.Features["proxy"]).To(BeFalse()) + + // Kafka topics at top-level + Expect(m.Kafka.Topics).To(HaveLen(4)) + Expect(m.Kafka.Topics[0].Name).To(Equal("filestream")) + Expect(m.Kafka.Topics[0].Topic).To(Equal("filestream")) + Expect(m.Kafka.Topics[0].PartitionCount).To(Equal(96)) + Expect(m.Kafka.Topics[0].Features).To(ContainElement("filestreamQueue")) + + // Applications basic presence + Expect(m.Applications).NotTo(BeEmpty()) + // Find api app + api := m.Applications["api"] + + Expect(api).NotTo(BeNil()) + Expect(api.InitContainers).To(BeEmpty()) + + // Migrations + Expect(m.Migrations).To(HaveKey("gorilla")) + Expect(m.Migrations).To(HaveKey("weave-trace")) + Expect(m.Migrations["gorilla"].Image.Repository).To(Equal("us-docker.pkg.dev/wandb-production/public/wandb/megabinary")) + Expect(m.Migrations["gorilla"].Args).To(ContainElement("migrate")) + + // Sizing comes from the split sizing.yaml file. + Expect(m.Kafka.Sizing["default"].Replicas).To(Equal(int32(2))) + Expect(m.Bucket["default"].Sizing["default"].Replicas).To(Equal(int32(1))) + }) +}) diff --git a/pkg/wandb/manifest/manifest_suite_test.go b/pkg/wandb/manifest/manifest_suite_test.go new file mode 100644 index 00000000..9fc37585 --- /dev/null +++ b/pkg/wandb/manifest/manifest_suite_test.go @@ -0,0 +1,13 @@ +package manifest_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestManifest(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Manifest YAML Decode Suite") +} diff --git a/pkg/wandb/spec/channel/deployer/deployer_test.go b/pkg/wandb/spec/channel/deployer/deployer_test.go index 5e23fd53..a6952b06 100644 --- a/pkg/wandb/spec/channel/deployer/deployer_test.go +++ b/pkg/wandb/spec/channel/deployer/deployer_test.go @@ -223,7 +223,8 @@ var _ = Describe("DeployerClient", func() { Expect(resp.StatusCode).To(Equal(http.StatusOK)) // Close the response body - io.Copy(io.Discard, resp.Body) + _, err = io.Copy(io.Discard, resp.Body) + Expect(err).NotTo(HaveOccurred()) resp.Body.Close() }) diff --git a/pkg/wandb/spec/charts/local.go b/pkg/wandb/spec/charts/local.go index 8471828f..9153944d 100644 --- a/pkg/wandb/spec/charts/local.go +++ b/pkg/wandb/spec/charts/local.go @@ -9,8 +9,8 @@ import ( v1 "github.com/wandb/operator/api/v1" "github.com/wandb/operator/pkg/helm" "github.com/wandb/operator/pkg/wandb/spec" - chart "helm.sh/helm/v4/pkg/chart/v2" - "helm.sh/helm/v4/pkg/chart/v2/loader" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/pkg/wandb/spec/charts/oci.go b/pkg/wandb/spec/charts/oci.go index 5dca4582..8854b40c 100644 --- a/pkg/wandb/spec/charts/oci.go +++ b/pkg/wandb/spec/charts/oci.go @@ -11,9 +11,9 @@ import ( v1 "github.com/wandb/operator/api/v1" "github.com/wandb/operator/pkg/helm" "github.com/wandb/operator/pkg/wandb/spec" - chart "helm.sh/helm/v4/pkg/chart/v2" - "helm.sh/helm/v4/pkg/chart/v2/loader" - "helm.sh/helm/v4/pkg/registry" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/registry" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" orasregistry "oras.land/oras-go/v2/registry" @@ -76,7 +76,7 @@ func (r OCIRelease) pullReference(registryClient *registry.Client) (string, erro return "", fmt.Errorf("invalid OCI URL %q: %w", r.URL, err) } - _, normalizedRef, err := registryClient.ValidateReference(r.URL, r.Version, parsedURL) + normalizedRef, err := registryClient.ValidateReference(r.URL, r.Version, parsedURL) if err != nil { return "", fmt.Errorf("failed to validate OCI reference %s: %w", r.URL, err) } diff --git a/pkg/wandb/spec/charts/oci_test.go b/pkg/wandb/spec/charts/oci_test.go index f56dea61..683d1c72 100644 --- a/pkg/wandb/spec/charts/oci_test.go +++ b/pkg/wandb/spec/charts/oci_test.go @@ -9,7 +9,7 @@ import ( . "github.com/onsi/gomega" v1 "github.com/wandb/operator/api/v1" "github.com/wandb/operator/pkg/wandb/spec" - "helm.sh/helm/v4/pkg/registry" + "helm.sh/helm/v3/pkg/registry" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/pkg/wandb/spec/charts/repo.go b/pkg/wandb/spec/charts/repo.go index bf85e6ab..85814eed 100644 --- a/pkg/wandb/spec/charts/repo.go +++ b/pkg/wandb/spec/charts/repo.go @@ -13,12 +13,12 @@ import ( v1 "github.com/wandb/operator/api/v1" "github.com/wandb/operator/pkg/helm" "github.com/wandb/operator/pkg/wandb/spec" - chart "helm.sh/helm/v4/pkg/chart/v2" - "helm.sh/helm/v4/pkg/cli" - "helm.sh/helm/v4/pkg/downloader" - "helm.sh/helm/v4/pkg/getter" - "helm.sh/helm/v4/pkg/registry" - repo "helm.sh/helm/v4/pkg/repo/v1" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/downloader" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/registry" + "helm.sh/helm/v3/pkg/repo" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" @@ -171,8 +171,8 @@ func (r RepoRelease) downloadChart() (string, error) { return "", err } - entry.InsecureSkipTLSVerify = parsedURL.Scheme == "http" - if entry.InsecureSkipTLSVerify && r.Debug { + entry.InsecureSkipTLSverify = parsedURL.Scheme == "http" + if entry.InsecureSkipTLSverify && r.Debug { log.Info("TLS verification disabled for HTTP URL", "url", r.URL) } @@ -203,7 +203,7 @@ func (r RepoRelease) downloadChart() (string, error) { getterOpts := []getter.Option{ getter.WithBasicAuth(r.Username, r.Password), - getter.WithInsecureSkipVerifyTLS(entry.InsecureSkipTLSVerify), + getter.WithInsecureSkipVerifyTLS(true), } providers := getter.All(settings) @@ -280,14 +280,16 @@ func (r RepoRelease) downloadChart() (string, error) { RegistryClient: cfg.RegistryClient, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, - ContentCache: settings.ContentCache, } dest := filepath.Join(os.Getenv("HELM_DATA_HOME"), "charts") if dest == "" { dest = "./charts" } - os.MkdirAll(dest, 0755) + if err := os.MkdirAll(dest, 0755); err != nil { + log.Error(err, "Failed to create charts directory", "destination", dest) + return "", err + } if r.Debug { log.Info("Attempting to download chart", "destination", dest) diff --git a/pkg/wandb/spec/charts/repo_test.go b/pkg/wandb/spec/charts/repo_test.go index 65ef8e75..b9c99a90 100644 --- a/pkg/wandb/spec/charts/repo_test.go +++ b/pkg/wandb/spec/charts/repo_test.go @@ -16,8 +16,8 @@ import ( . "github.com/onsi/gomega" v1 "github.com/wandb/operator/api/v1" "github.com/wandb/operator/pkg/wandb/spec" - chart "helm.sh/helm/v4/pkg/chart/v2" - repo "helm.sh/helm/v4/pkg/repo/v1" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/repo" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -326,7 +326,11 @@ var _ = Describe("RepoRelease", func() { }, }, } - json.NewEncoder(w).Encode(response) + err := json.NewEncoder(w).Encode(response) + if err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + return + } })) // Set up the repo release to use this server @@ -397,7 +401,11 @@ var _ = Describe("RepoRelease", func() { } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(indexFile) + err := json.NewEncoder(w).Encode(indexFile) + if err != nil { + http.Error(w, "Failed to encode index file", http.StatusInternalServerError) + return + } })) defer httpServer.Close() @@ -774,16 +782,3 @@ var _ = Describe("RepoRelease", func() { }) }) }) - -// recordingTransport is a test helper that wraps the default transport and records request information -type recordingTransport struct { - originalTransport http.RoundTripper - requestCallback func(*http.Request) -} - -func (t *recordingTransport) RoundTrip(req *http.Request) (*http.Response, error) { - if t.requestCallback != nil { - t.requestCallback(req) - } - return t.originalTransport.RoundTrip(req) -} diff --git a/pkg/wandb/spec/charts/repo_test_helpers.go b/pkg/wandb/spec/charts/repo_test_helpers.go index 099f1e6f..772e34fe 100644 --- a/pkg/wandb/spec/charts/repo_test_helpers.go +++ b/pkg/wandb/spec/charts/repo_test_helpers.go @@ -1,48 +1,11 @@ package charts import ( - "net/http" - "net/http/httptest" - "time" - "github.com/go-logr/logr" "github.com/onsi/ginkgo/v2" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" ) -type mockTransport struct { - responses map[string]*http.Response - timeout time.Duration -} - -func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { - if t.timeout > 0 { - time.Sleep(t.timeout) - } - resp := t.responses[req.URL.String()] - if resp == nil { - return &http.Response{ - StatusCode: http.StatusNotFound, - Body: http.NoBody, - }, nil - } - return resp, nil -} - -func setupMockHTTPClient(responses map[string]*http.Response) *http.Client { - return &http.Client{ - Transport: &mockTransport{ - responses: responses, - timeout: 1 * time.Millisecond, - }, - Timeout: 10 * time.Millisecond, - } -} - -func setupTestServer(handler http.HandlerFunc) *httptest.Server { - return httptest.NewServer(handler) -} - type logCapture struct{} func newLogCapture() *logCapture { diff --git a/pkg/wandb/spec/config.go b/pkg/wandb/spec/config.go index cca68907..2d3f935d 100644 --- a/pkg/wandb/spec/config.go +++ b/pkg/wandb/spec/config.go @@ -7,8 +7,8 @@ import ( "github.com/pkg/errors" "github.com/wandb/operator/pkg/utils" - chartutil "helm.sh/helm/v4/pkg/chart/common/util" - "helm.sh/helm/v4/pkg/strvals" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/strvals" ) // Values holds an arbitrary tree-like data structure, such as parsed JSON or @@ -142,7 +142,7 @@ func (v Values) SetValue(key string, value interface{}) error { cursor := v keyElements := strings.Split(key, ".") - var target interface{} = cursor + var target interface{} for idx, elm := range keyElements { target = cursor[elm] diff --git a/pkg/wandb/spec/spec.go b/pkg/wandb/spec/spec.go index 811c9de0..ae0dcf46 100644 --- a/pkg/wandb/spec/spec.go +++ b/pkg/wandb/spec/spec.go @@ -7,7 +7,7 @@ import ( "reflect" v1 "github.com/wandb/operator/api/v1" - chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v3/pkg/chart" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/pkg/wandb/spec/specfakes/fake_chart.go b/pkg/wandb/spec/specfakes/fake_chart.go index b78ae2f8..6a6b4a68 100644 --- a/pkg/wandb/spec/specfakes/fake_chart.go +++ b/pkg/wandb/spec/specfakes/fake_chart.go @@ -7,7 +7,7 @@ import ( v1 "github.com/wandb/operator/api/v1" "github.com/wandb/operator/pkg/wandb/spec" - v2 "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v3/pkg/chart" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -28,16 +28,16 @@ type FakeChart struct { applyReturnsOnCall map[int]struct { result1 error } - ChartStub func() (*v2.Chart, error) + ChartStub func() (*chart.Chart, error) chartMutex sync.RWMutex chartArgsForCall []struct { } chartReturns struct { - result1 *v2.Chart + result1 *chart.Chart result2 error } chartReturnsOnCall map[int]struct { - result1 *v2.Chart + result1 *chart.Chart result2 error } PruneStub func(context.Context, client.Client, *v1.WeightsAndBiases, *runtime.Scheme, spec.Values) error @@ -124,7 +124,7 @@ func (fake *FakeChart) ApplyReturnsOnCall(i int, result1 error) { }{result1} } -func (fake *FakeChart) Chart() (*v2.Chart, error) { +func (fake *FakeChart) Chart() (*chart.Chart, error) { fake.chartMutex.Lock() ret, specificReturn := fake.chartReturnsOnCall[len(fake.chartArgsForCall)] fake.chartArgsForCall = append(fake.chartArgsForCall, struct { @@ -148,34 +148,34 @@ func (fake *FakeChart) ChartCallCount() int { return len(fake.chartArgsForCall) } -func (fake *FakeChart) ChartCalls(stub func() (*v2.Chart, error)) { +func (fake *FakeChart) ChartCalls(stub func() (*chart.Chart, error)) { fake.chartMutex.Lock() defer fake.chartMutex.Unlock() fake.ChartStub = stub } -func (fake *FakeChart) ChartReturns(result1 *v2.Chart, result2 error) { +func (fake *FakeChart) ChartReturns(result1 *chart.Chart, result2 error) { fake.chartMutex.Lock() defer fake.chartMutex.Unlock() fake.ChartStub = nil fake.chartReturns = struct { - result1 *v2.Chart + result1 *chart.Chart result2 error }{result1, result2} } -func (fake *FakeChart) ChartReturnsOnCall(i int, result1 *v2.Chart, result2 error) { +func (fake *FakeChart) ChartReturnsOnCall(i int, result1 *chart.Chart, result2 error) { fake.chartMutex.Lock() defer fake.chartMutex.Unlock() fake.ChartStub = nil if fake.chartReturnsOnCall == nil { fake.chartReturnsOnCall = make(map[int]struct { - result1 *v2.Chart + result1 *chart.Chart result2 error }) } fake.chartReturnsOnCall[i] = struct { - result1 *v2.Chart + result1 *chart.Chart result2 error }{result1, result2} } diff --git a/pkg/wandb/spec/state/versioning.go b/pkg/wandb/spec/state/versioning.go index 668df97b..52ea2d37 100644 --- a/pkg/wandb/spec/state/versioning.go +++ b/pkg/wandb/spec/state/versioning.go @@ -3,6 +3,7 @@ package state import ( "context" "fmt" + "github.com/wandb/operator/pkg/wandb/spec" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" diff --git a/pkg/wandb/spec/state/versioning_test.go b/pkg/wandb/spec/state/versioning_test.go index b749b623..e97c8107 100644 --- a/pkg/wandb/spec/state/versioning_test.go +++ b/pkg/wandb/spec/state/versioning_test.go @@ -24,7 +24,7 @@ var _ = Describe("Versioning", func() { }) Describe("New", func() { It("should return a new Manager", func() { - m := state.New(nil, nil, nil, nil, nil) + m := state.New(context.TODO(), nil, nil, nil, nil) Expect(m).To(BeAssignableToTypeOf(&state.Manager{})) }) }) diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 7d9e135e..55e04644 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -41,6 +41,10 @@ var ( // isCertManagerAlreadyInstalled will be set true when CertManager CRDs be found on the cluster isCertManagerAlreadyInstalled = false + // USE_EXISTING_CLUSTER=true skips image build/load and Prometheus/CertManager installation, + // for use against a cluster already managed externally (e.g. Tilt). + useExistingCluster = os.Getenv("USE_EXISTING_CLUSTER") == "true" + // projectImage is the name of the image which will be build and loaded // with the code source changes to be tested. projectImage = "example.com/operator:v0.0.1" @@ -57,6 +61,10 @@ func TestE2E(t *testing.T) { } var _ = BeforeSuite(func() { + if useExistingCluster { + return + } + By("Ensure that Prometheus is enabled") _ = utils.UncommentCode("config/default/kustomization.yaml", "#- ../prometheus", "#") @@ -98,6 +106,9 @@ var _ = BeforeSuite(func() { }) var _ = AfterSuite(func() { + if useExistingCluster { + return + } // Teardown Prometheus and CertManager after the suite if not skipped and if they were not already installed if !skipPrometheusInstall && !isPrometheusOperatorAlreadyInstalled { _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling Prometheus Operator...\n") diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 2cfac8be..c9dc5038 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -261,6 +261,58 @@ var _ = Describe("Manager", Ordered, func() { )) }) + It("should provisioned cert-manager", func() { + By("validating that cert-manager has the certificate Secret") + verifyCertManager := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace) + _, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + } + Eventually(verifyCertManager).Should(Succeed()) + }) + + It("should have CA injection for mutating webhooks", func() { + By("checking CA injection for mutating webhooks") + verifyCAInjection := func(g Gomega) { + cmd := exec.Command("kubectl", "get", + "mutatingwebhookconfigurations.admissionregistration.k8s.io", + "operator-mutating-webhook-configuration", + "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}") + mwhOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(mwhOutput)).To(BeNumerically(">", 10)) + } + Eventually(verifyCAInjection).Should(Succeed()) + }) + + It("should have CA injection for validating webhooks", func() { + By("checking CA injection for validating webhooks") + verifyCAInjection := func(g Gomega) { + cmd := exec.Command("kubectl", "get", + "validatingwebhookconfigurations.admissionregistration.k8s.io", + "operator-validating-webhook-configuration", + "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}") + vwhOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(vwhOutput)).To(BeNumerically(">", 10)) + } + Eventually(verifyCAInjection).Should(Succeed()) + }) + + It("should have CA injection for WeightsAndBiases conversion webhook", func() { + By("checking CA injection for WeightsAndBiases conversion webhook") + verifyCAInjection := func(g Gomega) { + cmd := exec.Command("kubectl", "get", + "customresourcedefinitions.apiextensions.k8s.io", + "weightsandbiases.apps.wandb.com", + "-o", "go-template={{ .spec.conversion.webhook.clientConfig.caBundle }}") + vwhOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(vwhOutput)).To(BeNumerically(">", 10)) + } + Eventually(verifyCAInjection).Should(Succeed()) + }) + // +kubebuilder:scaffold:e2e-webhooks-checks // TODO: Customize the e2e test suite with scenarios specific to your project. diff --git a/test/e2e/retention_test.go b/test/e2e/retention_test.go new file mode 100644 index 00000000..572d0c3d --- /dev/null +++ b/test/e2e/retention_test.go @@ -0,0 +1,567 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "encoding/json" + "fmt" + "math/rand" + "os" + "os/exec" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/wandb/operator/test/utils" +) + +var retentionSuiteSetupDone bool + +const ( + retentionReadyTimeout = 15 * time.Minute + retentionDeleteTimeout = 5 * time.Minute + retentionPollingInterval = 10 * time.Second + + wandbNameLabel = "apps.wandb.com/name" + wandbNamespaceLabel = "apps.wandb.com/namespace" + wandbModuleLabel = "apps.wandb.com/module" +) + +// infraCRType maps a module name to its third-party Kubernetes CRD resource type. +var infraCRType = map[string]string{ + "moco": "innodbcluster", + "redis": "redis", + "kafka": "kafka", + "objectStore": "tenant", + "clickhouse": "clickhouseinstallation", +} + +var allModules = []string{"moco", "redis", "kafka", "objectStore", "clickhouse"} + +func generateUniqueName(prefix string) string { + const letters = "abcdefghijklmnopqrstuvwxyz" + suffix := make([]byte, 6) + for i := range suffix { + suffix[i] = letters[rand.Intn(len(letters))] //nolint:gosec + } + return fmt.Sprintf("%s-%d-%s", prefix, time.Now().Unix(), string(suffix)) +} + +func createNamespace(ns string) error { + cmd := exec.Command("kubectl", "create", "namespace", ns) + _, err := utils.Run(cmd) + return err +} + +func deleteNamespace(ns string) { + cmd := exec.Command("kubectl", "delete", "namespace", ns, "--ignore-not-found", "--wait=false") + _, _ = utils.Run(cmd) +} + +func buildLabelSelector(wandbName, wandbNs, module string) string { + return fmt.Sprintf("%s=%s,%s=%s,%s=%s", + wandbNameLabel, wandbName, + wandbNamespaceLabel, wandbNs, + wandbModuleLabel, module, + ) +} + +type manifestOpts struct { + name string + ns string + size string + specRetentionPolicy string + componentOverrides map[string]string +} + +func createWandbManifest(opts manifestOpts) string { + managedFieldName := map[string]string{ + "moco": "managedMysql", + "redis": "managedRedis", + "kafka": "managedKafka", + "objectStore": "managedObjectStore", + "clickhouse": "managedClickhouse", + } + componentBlock := func(module string) string { + managed := managedFieldName[module] + override := "" + if policy, ok := opts.componentOverrides[module]; ok { + override = fmt.Sprintf("\n retentionPolicy:\n onDelete: %s", policy) + } + return fmt.Sprintf(" %s:\n %s:%s", module, managed, override) + } + + return fmt.Sprintf(`apiVersion: apps.wandb.com/v2 +kind: WeightsAndBiases +metadata: + name: %s + namespace: %s +spec: + size: %s + retentionPolicy: + onDelete: %s + wandb: + features: {} +%s +%s +%s +%s +%s +`, opts.name, opts.ns, opts.size, opts.specRetentionPolicy, + componentBlock("moco"), + componentBlock("redis"), + componentBlock("kafka"), + componentBlock("objectStore"), + componentBlock("clickhouse"), + ) +} + +func applyManifest(manifest string) error { + f, err := os.CreateTemp("", "wandb-*.yaml") + if err != nil { + return err + } + defer os.Remove(f.Name()) + if _, err := f.WriteString(manifest); err != nil { + return err + } + f.Close() + cmd := exec.Command("kubectl", "apply", "-f", f.Name()) + _, err = utils.Run(cmd) + return err +} + +func deleteWandB(name, ns string) error { + cmd := exec.Command("kubectl", "delete", "weightsandbiases", name, "-n", ns, "--wait=false") + _, err := utils.Run(cmd) + return err +} + +func patchComponentDisabled(wandbName, ns, module string) error { + patch := fmt.Sprintf(`{"spec":{%q:{"enabled":false}}}`, module) + cmd := exec.Command("kubectl", "patch", "weightsandbiases", wandbName, + "-n", ns, "--type=merge", "-p", patch) + _, err := utils.Run(cmd) + return err +} + +// waitForWandbReady polls until all five component statuses report "Ready". +func waitForWandbReady(name, ns string) { + verifyReady := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "weightsandbiases", name, "-n", ns, "-o", "json") + output, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + + var obj map[string]any + g.Expect(json.Unmarshal([]byte(output), &obj)).To(Succeed()) + + status, ok := obj["status"].(map[string]any) + g.Expect(ok).To(BeTrue(), "status field missing") + + statusKeys := map[string]string{ + "moco": "mysqlStatus", + "redis": "redisStatus", + "kafka": "kafkaStatus", + "objectStore": "objectStoreStatus", + "clickhouse": "clickhouseStatus", + } + for _, key := range statusKeys { + cs, ok := status[key].(map[string]any) + g.Expect(ok).To(BeTrue(), "component status %s missing", key) + g.Expect(cs["state"]).To(Equal("Healthy"), "component %s not Healthy", key) + } + } + Eventually(verifyReady, retentionReadyTimeout, retentionPollingInterval).Should(Succeed()) +} + +func resourceItemCount(resourceType, labelSelector, ns string) int { + cmd := exec.Command("kubectl", "get", resourceType, + "-l", labelSelector, "-n", ns, "-o", "json") + output, err := utils.Run(cmd) + if err != nil { + return -1 + } + var result map[string]any + if err := json.Unmarshal([]byte(output), &result); err != nil { + return -1 + } + items, _ := result["items"].([]any) + return len(items) +} + +// resourceItemCountInNs counts all resources of the given type in a namespace without label filtering. +// Used for infra CRs that are not labeled with WandB metadata labels. +func resourceItemCountInNs(resourceType, ns string) int { + cmd := exec.Command("kubectl", "get", resourceType, "-n", ns, "-o", "json") + output, err := utils.Run(cmd) + if err != nil { + return -1 + } + var result map[string]any + if err := json.Unmarshal([]byte(output), &result); err != nil { + return -1 + } + items, _ := result["items"].([]any) + return len(items) +} + +func verifyComponentResourcesExist(wandbName, ns, module string, g Gomega) { + sel := buildLabelSelector(wandbName, ns, module) + g.Expect(resourceItemCount("pvc", sel, ns)).To(BeNumerically(">", 0), + "expected PVCs for module %s", module) + g.Expect(resourceItemCountInNs(infraCRType[module], ns)).To(BeNumerically(">", 0), + "expected infra CR %s for module %s", infraCRType[module], module) +} + +func verifyComponentResourcesDeleted(wandbName, ns, module string, timeout time.Duration) { + sel := buildLabelSelector(wandbName, ns, module) + verifyGone := func(g Gomega) { + g.Expect(resourceItemCount("pvc", sel, ns)).To(Equal(0), + "PVCs still exist for module %s", module) + g.Expect(resourceItemCountInNs(infraCRType[module], ns)).To(Equal(0), + "infra CR %s still exists for module %s", infraCRType[module], module) + } + Eventually(verifyGone, timeout, retentionPollingInterval).Should(Succeed()) +} + +func verifyWandbDeleted(name, ns string) { + verifyGone := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "weightsandbiases", name, "-n", ns) + _, err := utils.Run(cmd) + g.Expect(err).To(HaveOccurred(), "WandB CR should no longer exist") + } + Eventually(verifyGone, retentionDeleteTimeout, retentionPollingInterval).Should(Succeed()) +} + +var _ = Describe("Retention Policy Integration Tests", func() { + + BeforeEach(func() { + if retentionSuiteSetupDone { + return + } + + By("ensuring WeightsAndBiases CRD is installed") + cmd := exec.Command("kubectl", "get", "crd", "weightsandbiases.apps.wandb.com") + if _, err := utils.Run(cmd); err != nil { + By("CRD not found — installing from deploy/operator/crds") + projectDir, err := utils.GetProjectDir() + Expect(err).NotTo(HaveOccurred()) + cmd = exec.Command("kubectl", "apply", "-f", + projectDir+"/deploy/operator/crds/apps.wandb.com_weightsandbiases.yaml", + "--server-side") + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to install WeightsAndBiases CRD") + } + + By("ensuring the WandB operator controller is running") + cmd = exec.Command("kubectl", "get", "pods", + "-l", "control-plane=controller-manager", + "-A", + "--field-selector=status.phase=Running", + "-o", "name") + output, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to check for operator controller pod") + Expect(utils.GetNonEmptyLines(output)).NotTo(BeEmpty(), + "No running operator controller pod found — deploy the WandB operator before running retention tests") + + retentionSuiteSetupDone = true + }) + + // setupWandbCR creates the namespace and applies the WandB CR manifest, returning cleanup func. + setupWandbCR := func(wandbName, wandbNs *string, namePrefix, size, specPolicy string, overrides map[string]string) { + BeforeEach(func() { + *wandbName = generateUniqueName(namePrefix) + *wandbNs = generateUniqueName("wandb-test") + + By(fmt.Sprintf("creating namespace %s", *wandbNs)) + Expect(createNamespace(*wandbNs)).To(Succeed()) + + By(fmt.Sprintf("creating WandB CR %s (size=%s, specPolicy=%s)", *wandbName, size, specPolicy)) + manifest := createWandbManifest(manifestOpts{ + name: *wandbName, + ns: *wandbNs, + size: size, + specRetentionPolicy: specPolicy, + componentOverrides: overrides, + }) + Expect(applyManifest(manifest)).To(Succeed()) + }) + + AfterEach(func() { + deleteNamespace(*wandbNs) + }) + } + + // --- Purge: CR deletion --- + runPurgeCRDeletionTest := func(size string) { + var ( + wandbName string + wandbNs string + ) + setupWandbCR(&wandbName, &wandbNs, fmt.Sprintf("wandb-%s-purge", size), size, "purge", nil) + + It("should purge all resources when the CR is deleted", func() { + By("waiting for all components to be Ready") + waitForWandbReady(wandbName, wandbNs) + + By("verifying all component resources exist") + for _, module := range allModules { + verifyComponentResourcesExist(wandbName, wandbNs, module, Default) + } + + By("deleting the WandB CR") + Expect(deleteWandB(wandbName, wandbNs)).To(Succeed()) + + By("verifying all component resources are purged") + for _, module := range allModules { + verifyComponentResourcesDeleted(wandbName, wandbNs, module, retentionDeleteTimeout) + } + + By("verifying the WandB CR itself is gone") + verifyWandbDeleted(wandbName, wandbNs) + }) + } + + // --- Purge: component disable --- + runPurgeComponentDisableTest := func(size, module string) { + var ( + wandbName string + wandbNs string + ) + setupWandbCR(&wandbName, &wandbNs, fmt.Sprintf("wandb-%s-%s-dis", size, module), size, "purge", nil) + + It(fmt.Sprintf("should purge %s resources when the component is disabled", module), func() { + By("waiting for all components to be Ready") + waitForWandbReady(wandbName, wandbNs) + + By("verifying all component resources exist") + for _, m := range allModules { + verifyComponentResourcesExist(wandbName, wandbNs, m, Default) + } + + By(fmt.Sprintf("disabling the %s component", module)) + Expect(patchComponentDisabled(wandbName, wandbNs, module)).To(Succeed()) + + By(fmt.Sprintf("verifying %s resources are purged", module)) + verifyComponentResourcesDeleted(wandbName, wandbNs, module, retentionDeleteTimeout) + + By("verifying remaining components are unaffected") + for _, m := range allModules { + if m == module { + continue + } + verifyComponentResourcesExist(wandbName, wandbNs, m, Default) + } + }) + } + + // --- Detach: CR deletion --- + runDetachCRDeletionTest := func(size string) { + var ( + wandbName string + wandbNs string + ) + setupWandbCR(&wandbName, &wandbNs, fmt.Sprintf("wandb-%s-detach", size), size, "detach", nil) + + It("should detach all resources when the CR is deleted", func() { + By("waiting for all components to be Ready") + waitForWandbReady(wandbName, wandbNs) + + By("verifying all component resources exist") + for _, module := range allModules { + verifyComponentResourcesExist(wandbName, wandbNs, module, Default) + } + + By("deleting the WandB CR") + Expect(deleteWandB(wandbName, wandbNs)).To(Succeed()) + + By("verifying the WandB CR itself is gone") + verifyWandbDeleted(wandbName, wandbNs) + + By("verifying all component resources still exist (detached, not purged)") + for _, module := range allModules { + verifyComponentResourcesExist(wandbName, wandbNs, module, Default) + } + }) + } + + // --- Detach: component disable --- + runDetachComponentDisableTest := func(size, module string) { + var ( + wandbName string + wandbNs string + ) + setupWandbCR(&wandbName, &wandbNs, fmt.Sprintf("wandb-%s-%s-det", size, module), size, "detach", nil) + + It(fmt.Sprintf("should detach %s resources when the component is disabled", module), func() { + By("waiting for all components to be Ready") + waitForWandbReady(wandbName, wandbNs) + + By("verifying all component resources exist") + for _, m := range allModules { + verifyComponentResourcesExist(wandbName, wandbNs, m, Default) + } + + By(fmt.Sprintf("disabling the %s component", module)) + Expect(patchComponentDisabled(wandbName, wandbNs, module)).To(Succeed()) + + Consistently(func(g Gomega) { + verifyComponentResourcesExist(wandbName, wandbNs, module, g) + }, 30*time.Second, retentionPollingInterval).Should(Succeed(), + "%s resources should survive disable with detach policy", module) + + By("verifying remaining components are unaffected") + for _, m := range allModules { + if m == module { + continue + } + verifyComponentResourcesExist(wandbName, wandbNs, m, Default) + } + }) + } + + // --- Component-level override: spec=detach, component=purge --- + runComponentOverridePurgeTest := func(size, module string) { + var ( + wandbName string + wandbNs string + ) + overrides := map[string]string{module: "purge"} + setupWandbCR(&wandbName, &wandbNs, fmt.Sprintf("wandb-%s-%s-ovp", size, module), size, "detach", overrides) + + It(fmt.Sprintf("should purge %s (override) while detaching others on CR deletion", module), func() { + By("waiting for all components to be Ready") + waitForWandbReady(wandbName, wandbNs) + + By("verifying all component resources exist") + for _, m := range allModules { + verifyComponentResourcesExist(wandbName, wandbNs, m, Default) + } + + By("deleting the WandB CR") + Expect(deleteWandB(wandbName, wandbNs)).To(Succeed()) + + By("verifying the WandB CR itself is gone") + verifyWandbDeleted(wandbName, wandbNs) + + By(fmt.Sprintf("verifying %s resources are purged (component override)", module)) + verifyComponentResourcesDeleted(wandbName, wandbNs, module, retentionDeleteTimeout) + + By("verifying other components still exist (detached)") + for _, m := range allModules { + if m == module { + continue + } + verifyComponentResourcesExist(wandbName, wandbNs, m, Default) + } + }) + } + + // --- Component-level override: spec=purge, component=detach --- + runComponentOverrideDetachTest := func(size, module string) { + var ( + wandbName string + wandbNs string + ) + overrides := map[string]string{module: "detach"} + setupWandbCR(&wandbName, &wandbNs, fmt.Sprintf("wandb-%s-%s-ovd", size, module), size, "purge", overrides) + + It(fmt.Sprintf("should detach %s (override) while purging others on CR deletion", module), func() { + By("waiting for all components to be Ready") + waitForWandbReady(wandbName, wandbNs) + + By("verifying all component resources exist") + for _, m := range allModules { + verifyComponentResourcesExist(wandbName, wandbNs, m, Default) + } + + By("deleting the WandB CR") + Expect(deleteWandB(wandbName, wandbNs)).To(Succeed()) + + By("verifying the WandB CR itself is gone") + verifyWandbDeleted(wandbName, wandbNs) + + By(fmt.Sprintf("verifying %s resources still exist (component detach override)", module)) + verifyComponentResourcesExist(wandbName, wandbNs, module, Default) + + By("verifying other components are purged") + for _, m := range allModules { + if m == module { + continue + } + verifyComponentResourcesDeleted(wandbName, wandbNs, m, retentionDeleteTimeout) + } + }) + } + + for _, size := range []string{"dev", "small"} { + size := size + + Context(fmt.Sprintf("%s size", size), func() { + Context("spec-level purge policy", func() { + Context("CR deletion", func() { + runPurgeCRDeletionTest(size) + }) + + Context("component disable", func() { + for _, module := range allModules { + module := module + Context(fmt.Sprintf("%s component", module), func() { + runPurgeComponentDisableTest(size, module) + }) + } + }) + }) + + Context("spec-level detach policy", func() { + Context("CR deletion", func() { + runDetachCRDeletionTest(size) + }) + + Context("component disable", func() { + for _, module := range allModules { + module := module + Context(fmt.Sprintf("%s component", module), func() { + runDetachComponentDisableTest(size, module) + }) + } + }) + }) + + Context("component-level overrides", func() { + Context("spec=detach with component=purge override", func() { + for _, module := range allModules { + module := module + Context(fmt.Sprintf("%s component", module), func() { + runComponentOverridePurgeTest(size, module) + }) + } + }) + + Context("spec=purge with component=detach override", func() { + for _, module := range allModules { + module := module + Context(fmt.Sprintf("%s component", module), func() { + runComponentOverrideDetachTest(size, module) + }) + } + }) + }) + }) + } +}) diff --git a/test/utils/utils.go b/test/utils/utils.go index 04a5141c..fbc7a1fa 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -24,7 +24,7 @@ import ( "os/exec" "strings" - . "github.com/onsi/ginkgo/v2" //nolint:golint,revive + . "github.com/onsi/ginkgo/v2" //nolint:all ) const ( @@ -197,7 +197,7 @@ func GetProjectDir() (string, error) { if err != nil { return wd, err } - wd = strings.Replace(wd, "/test/e2e", "", -1) + wd = strings.ReplaceAll(wd, "/test/e2e", "") return wd, nil } diff --git a/tilt-settings.sample.json b/tilt-settings.sample.json deleted file mode 100644 index a9c794dc..00000000 --- a/tilt-settings.sample.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "allowedContexts": [ - "docker-desktop", - "minikube", - "kind-kind", - "orbstack" - ], - "installMinio": true, - "installWandb": true, - "wandbCRD": "default" -} diff --git a/tilt-settings.sample.star b/tilt-settings.sample.star new file mode 100644 index 00000000..4aab4811 --- /dev/null +++ b/tilt-settings.sample.star @@ -0,0 +1,56 @@ +SETTINGS = { + "allowedContexts": ["docker-desktop", "minikube", "kind-kind", "kind-wandb-operator", "orbstack", "crc-admin"], + + # Operator install settings. + "operatorNamespace": "wandb-operators", + "openshiftSCC": False, + + # W&B instance settings. + "includeCR": True, + + # Optional base WeightsAndBiases CR YAML. Tilt patches it with the scalar + # settings below. + "crFile": "", + "wandbName": "wandb", + "wandbNamespace": "wandb", + "wandbHostname": "http://localhost:8080", + "wandbVersion": "0.80.0", + "size": "dev", + "retentionPolicy": "detach", + "licenseFile": "", + + # Default to the published server manifest repository. Use + # local mode only when developing against repo-local manifest definitions. + "manifestSource": "published", + "localManifestPath": "hack/testing-manifests/server-manifest", + + # Choose the local networking path. Tilt installs the matching local + # dependency automatically: nginx-gateway-fabric for "gateway", or + # ingress-nginx for "ingress". Ingress mode defaults to + # http://wandb.localhost:8080 unless wandbHostname is set explicitly. + "networkMode": "gateway", + + # Defaults for the generated CR. These usually only need to change when + # matching an existing local GatewayClass or IngressClass. + "gatewayClass": "nginx", + "ingressClass": "nginx", + + # off, full, or forward. "full" enables VictoriaMetrics/Grafana operators + # and exposes local telemetry endpoint resources. + "observabilityMode": "off", + + "logFormat": "pretty", + + # Optional composable Tilt infra settings. External infra installs the + # local test-infra chart for the selected service. useCustomCA generates + # test CA material through the normal W&B CR and user ConfigMap inputs. + "useExternalMysql": False, + "useExternalRedis": False, + "useExternalObjectStore": False, + "useCustomCA": False, + + # CRC/OpenShift Local uses the crc-admin context. Tilt auto-enables + # openshiftSCC on CRC; set it explicitly for other OpenShift clusters. + # "allowedContexts": ["crc-admin"], + # "openshiftSCC": True, +} From 681b7d705b7095c07d6253a9a5d21e8ace9a18c6 Mon Sep 17 00:00:00 2001 From: Jagat Thakkar <32109558+jthakkar04@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:45:40 -0500 Subject: [PATCH 17/49] feat: Creating a Beta Release (#262) --- deploy/operator/Chart.yaml | 4 ++-- deploy/operator/values.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/operator/Chart.yaml b/deploy/operator/Chart.yaml index f280ab02..a2f878a8 100644 --- a/deploy/operator/Chart.yaml +++ b/deploy/operator/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: operator description: A Helm chart for Weights & Biases operator type: application -version: 2.0.0-alpha.2 -appVersion: "2.0.0-alpha.2" +version: 2.0.0-beta.1 +appVersion: "2.0.0-beta.1" maintainers: - name: wandb email: support@wandb.com diff --git a/deploy/operator/values.yaml b/deploy/operator/values.yaml index fc546967..f6c1c1b9 100644 --- a/deploy/operator/values.yaml +++ b/deploy/operator/values.yaml @@ -9,13 +9,13 @@ wandb: install: true size: small namespace: wandb - version: 0.81.0 + version: 0.82.2 internalServiceAuth: false wandb-operator: image: repository: us-docker.pkg.dev/wandb-production/public/wandb/operator - tag: 2.0.0-alpha.2 + tag: 2.0.0-beta.1 containers: operator: command: From b08c69cc4808fe7621386154bbe8c32427529b2b Mon Sep 17 00:00:00 2001 From: Casey Abernathy <cabernathy@coreweave.com> Date: Wed, 15 Jul 2026 10:56:44 -0700 Subject: [PATCH 18/49] fix: loosen release naming to allow subversions with semver (#266) --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index bec88a32..ac8cc928 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -36,7 +36,7 @@ jobs: set -euo pipefail tag="${GITHUB_REF_NAME}" - if [[ ! "${tag}" =~ ^v2\.([0-9]+)\.([0-9]+)$ ]]; then + if [[ ! "${tag}" =~ ^v2\.([0-9]+)\.([0-9]+)(\-.*)?$ ]]; then echo "Expected a stable v2 tag in the form v2.x.y; got ${tag}" >&2 exit 1 fi From 75970ae05b89ec6158c495309cd47a3f009b255e Mon Sep 17 00:00:00 2001 From: Casey Abernathy <cabernathy@coreweave.com> Date: Wed, 15 Jul 2026 11:20:55 -0700 Subject: [PATCH 19/49] fix: Checkout tags in release pipelines (#267) --- .github/workflows/release.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ac8cc928..4368f020 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -27,6 +27,7 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 + fetch-tags: true persist-credentials: false - name: Validate tag and release versions From 5d439bdcd42cd487adf8cc0a38abb6c2b9b42cc7 Mon Sep 17 00:00:00 2001 From: Casey Abernathy <cabernathy@coreweave.com> Date: Wed, 15 Jul 2026 11:30:15 -0700 Subject: [PATCH 20/49] fix: force tag checkouts in release pipeline (#269) --- .github/workflows/release.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4368f020..ce49fb7a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -37,11 +37,14 @@ jobs: set -euo pipefail tag="${GITHUB_REF_NAME}" + if [[ ! "${tag}" =~ ^v2\.([0-9]+)\.([0-9]+)(\-.*)?$ ]]; then echo "Expected a stable v2 tag in the form v2.x.y; got ${tag}" >&2 exit 1 fi + git fetch origin "refs/tags/${tag}:refs/tags/${tag}" --no-tags + if [[ "$(git cat-file -t "refs/tags/${tag}")" != "tag" ]]; then echo "Release tag ${tag} must be annotated" >&2 exit 1 From 9ed575a2de66a02abb440513a3471038a2270a9f Mon Sep 17 00:00:00 2001 From: Casey Abernathy <cabernathy@coreweave.com> Date: Wed, 15 Jul 2026 11:37:57 -0700 Subject: [PATCH 21/49] fix: force tag checkout in release (#270) --- .github/workflows/release.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ce49fb7a..667a7f88 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -43,14 +43,15 @@ jobs: exit 1 fi - git fetch origin "refs/tags/${tag}:refs/tags/${tag}" --no-tags + git fetch --no-tags --force origin "refs/tags/${tag}:refs/tags/${tag}" if [[ "$(git cat-file -t "refs/tags/${tag}")" != "tag" ]]; then echo "Release tag ${tag} must be annotated" >&2 exit 1 fi - git fetch --no-tags origin refs/heads/main:refs/remotes/origin/main + git fetch --no-tags --force origin refs/heads/main:refs/remotes/origin/main + tagged_commit="$(git rev-list -n 1 "${tag}")" if ! git merge-base --is-ancestor "${tagged_commit}" refs/remotes/origin/main; then echo "Tagged commit ${tagged_commit} is not reachable from origin/main" >&2 From 63c869ae9e8554fcbd81d4fccfedb5ac5ca0b7f8 Mon Sep 17 00:00:00 2001 From: casey-coreweave <cabernathy@coreweave.com> Date: Wed, 15 Jul 2026 11:53:46 -0700 Subject: [PATCH 22/49] fix: fixup chart dependencies --- .github/workflows/release.yaml | 62 +++++++++++++++++----------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 667a7f88..c8c7a2df 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -100,36 +100,36 @@ jobs: - name: Authorize Docker for Artifact Registry run: gcloud auth configure-docker us-docker.pkg.dev --quiet - - name: Reject existing production artifacts - env: - VERSION: ${{ steps.release.outputs.version }} - shell: bash - run: | - set -euo pipefail - - assert_absent() { - local artifact=$1 - local output - local status - - set +e - output="$(gcloud artifacts docker images describe "${artifact}" --format='value(image_summary.digest)' 2>&1)" - status=$? - set -e - - if [[ ${status} -eq 0 ]]; then - echo "Refusing to overwrite existing artifact ${artifact}" >&2 - exit 1 - fi - if ! grep -Eqi 'NOT_FOUND|not found' <<< "${output}"; then - echo "Could not safely determine whether ${artifact} exists:" >&2 - echo "${output}" >&2 - exit 1 - fi - } - - assert_absent "${IMAGE_REPOSITORY}:${VERSION}" - assert_absent "${CHART_REPOSITORY}/operator:${VERSION}" + # - name: Reject existing production artifacts + # env: + # VERSION: ${{ steps.release.outputs.version }} + # shell: bash + # run: | + # set -euo pipefail + + # assert_absent() { + # local artifact=$1 + # local output + # local status + + # set +e + # output="$(gcloud artifacts docker images describe "${artifact}" --format='value(image_summary.digest)' 2>&1)" + # status=$? + # set -e + + # if [[ ${status} -eq 0 ]]; then + # echo "Refusing to overwrite existing artifact ${artifact}" >&2 + # exit 1 + # fi + # if ! grep -Eqi 'NOT_FOUND|not found' <<< "${output}"; then + # echo "Could not safely determine whether ${artifact} exists:" >&2 + # echo "${output}" >&2 + # exit 1 + # fi + # } + + # assert_absent "${IMAGE_REPOSITORY}:${VERSION}" + # assert_absent "${CHART_REPOSITORY}/operator:${VERSION}" - name: Build and publish image env: @@ -141,7 +141,7 @@ jobs: VERSION: ${{ steps.release.outputs.version }} run: | set -euo pipefail - helm dependency build deploy/operator + helm dependency build deploy/operator --update-dependencies mkdir -p dist helm package deploy/operator --destination dist helm push "dist/operator-${VERSION}.tgz" "oci://${CHART_REPOSITORY}" From 79b1ae75df144ef61173500a8c13f736e186e2ed Mon Sep 17 00:00:00 2001 From: casey-coreweave <cabernathy@coreweave.com> Date: Wed, 15 Jul 2026 12:00:28 -0700 Subject: [PATCH 23/49] fix: update helm deps explicitly in release --- .github/workflows/release.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c8c7a2df..a42cad11 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -141,7 +141,8 @@ jobs: VERSION: ${{ steps.release.outputs.version }} run: | set -euo pipefail - helm dependency build deploy/operator --update-dependencies + helm dependency update deploy/operator + helm dependency build deploy/operator mkdir -p dist helm package deploy/operator --destination dist helm push "dist/operator-${VERSION}.tgz" "oci://${CHART_REPOSITORY}" From c60e3ac91508d486dfaa4b761ba408d0fbc18eb8 Mon Sep 17 00:00:00 2001 From: casey-coreweave <cabernathy@coreweave.com> Date: Wed, 15 Jul 2026 12:12:06 -0700 Subject: [PATCH 24/49] fix: add chart-testing to release flow --- .github/workflows/release.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a42cad11..a2873a02 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -136,12 +136,19 @@ jobs: VERSION: ${{ steps.release.outputs.version }} run: make docker-build docker-push IMG="${IMAGE_REPOSITORY}:${VERSION}" + - name: Install chart-testing + uses: helm/chart-testing-action@e6669bcd63d7cb57cb4380c33043eebe5d111992 # v2.6.1 + with: + version: v3.14.0 + + - name: Lint charts + run: ct lint --all --config deploy/ct.yaml + - name: Package and publish chart env: VERSION: ${{ steps.release.outputs.version }} run: | set -euo pipefail - helm dependency update deploy/operator helm dependency build deploy/operator mkdir -p dist helm package deploy/operator --destination dist From 7da7c58dea7d2507d1ed2b30f1f69a2421df1898 Mon Sep 17 00:00:00 2001 From: casey-coreweave <cabernathy@coreweave.com> Date: Wed, 15 Jul 2026 12:19:32 -0700 Subject: [PATCH 25/49] fix: check for existing artifacts in release flows --- .github/workflows/release.yaml | 60 +++++++++++++++++----------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a2873a02..9b6e9f72 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -100,36 +100,36 @@ jobs: - name: Authorize Docker for Artifact Registry run: gcloud auth configure-docker us-docker.pkg.dev --quiet - # - name: Reject existing production artifacts - # env: - # VERSION: ${{ steps.release.outputs.version }} - # shell: bash - # run: | - # set -euo pipefail - - # assert_absent() { - # local artifact=$1 - # local output - # local status - - # set +e - # output="$(gcloud artifacts docker images describe "${artifact}" --format='value(image_summary.digest)' 2>&1)" - # status=$? - # set -e - - # if [[ ${status} -eq 0 ]]; then - # echo "Refusing to overwrite existing artifact ${artifact}" >&2 - # exit 1 - # fi - # if ! grep -Eqi 'NOT_FOUND|not found' <<< "${output}"; then - # echo "Could not safely determine whether ${artifact} exists:" >&2 - # echo "${output}" >&2 - # exit 1 - # fi - # } - - # assert_absent "${IMAGE_REPOSITORY}:${VERSION}" - # assert_absent "${CHART_REPOSITORY}/operator:${VERSION}" + - name: Reject existing production artifacts + env: + VERSION: ${{ steps.release.outputs.version }} + shell: bash + run: | + set -euo pipefail + + assert_absent() { + local artifact=$1 + local output + local status + + set +e + output="$(gcloud artifacts docker images describe "${artifact}" --format='value(image_summary.digest)' 2>&1)" + status=$? + set -e + + if [[ ${status} -eq 0 ]]; then + echo "Refusing to overwrite existing artifact ${artifact}" >&2 + exit 1 + fi + if ! grep -Eqi 'NOT_FOUND|not found' <<< "${output}"; then + echo "Could not safely determine whether ${artifact} exists:" >&2 + echo "${output}" >&2 + exit 1 + fi + } + + assert_absent "${IMAGE_REPOSITORY}:${VERSION}" + assert_absent "${CHART_REPOSITORY}/operator:${VERSION}" - name: Build and publish image env: From 6ba64d13de205da8a2ff2bfb1a6afc00436b36da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:35:45 -0700 Subject: [PATCH 26/49] chore(deps): bump github.com/onsi/gomega from 1.39.1 to 1.42.1 (#254) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 18 +++++++++--------- go.sum | 36 ++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 71bab55d..0da81b78 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 github.com/nginx/nginx-gateway-fabric v1.6.2 github.com/onsi/ginkgo/v2 v2.28.1 - github.com/onsi/gomega v1.39.1 + github.com/onsi/gomega v1.42.1 github.com/opencontainers/image-spec v1.1.1 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.23.2 @@ -30,7 +30,7 @@ require ( github.com/twmb/franz-go v1.21.3 github.com/twmb/franz-go/pkg/kadm v1.18.0 golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 - golang.org/x/text v0.37.0 + golang.org/x/text v0.38.0 gopkg.in/d4l3k/messagediff.v1 v1.2.1 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.19.2 @@ -171,15 +171,15 @@ require ( go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.51.0 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.53.0 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/net v0.56.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.44.0 // indirect - golang.org/x/term v0.43.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/tools v0.45.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect diff --git a/go.sum b/go.sum index 1c12ef9c..1e7ea24d 100644 --- a/go.sum +++ b/go.sum @@ -282,8 +282,8 @@ github.com/nginx/nginx-gateway-fabric v1.6.2 h1:ktdShWxT/Drh/5/u8S5QMRgnnBGvVuFD github.com/nginx/nginx-gateway-fabric v1.6.2/go.mod h1:Fi2hdmoNj9nQRX9YQDju+ntMPG4Fgcw+irfl/GYWSEk= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= +github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -441,30 +441,30 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= From e0f10c3e37d26c69d4badbd5a49b3bccc68b23df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:36:50 -0700 Subject: [PATCH 27/49] chore(deps): bump github.com/maxbrunsfeld/counterfeiter/v6 from 6.11.2 to 6.12.2 (#255) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0da81b78..fa7a60e3 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/imdario/mergo v0.3.16 github.com/kedacore/keda/v2 v2.18.3 github.com/lmittmann/tint v1.1.2 - github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 + github.com/maxbrunsfeld/counterfeiter/v6 v6.12.2 github.com/nginx/nginx-gateway-fabric v1.6.2 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.42.1 diff --git a/go.sum b/go.sum index 1e7ea24d..3d2fb3e1 100644 --- a/go.sum +++ b/go.sum @@ -254,8 +254,8 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 h1:yVCLo4+ACVroOEr4iFU1iH46Ldlzz2rTuu18Ra7M8sU= -github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2/go.mod h1:VzB2VoMh1Y32/QqDfg9ZJYHj99oM4LiGtqPZydTiQSQ= +github.com/maxbrunsfeld/counterfeiter/v6 v6.12.2 h1:V23nK2R2B63g2GhygF9zVGpnigmhvoZoH8d0hrZwMGY= +github.com/maxbrunsfeld/counterfeiter/v6 v6.12.2/go.mod h1:Mr897yU9FmyKaQDPtRlVKibrjz40XXyOHUfyZBPSyZU= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc= From be936fbeafd016b4785379319d32c29dcfc51a78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:37:31 -0700 Subject: [PATCH 28/49] chore(deps): bump github.com/expr-lang/expr from 1.17.6 to 1.17.7 (#263) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index fa7a60e3..50a1475a 100644 --- a/go.mod +++ b/go.mod @@ -74,7 +74,7 @@ require ( github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/expr-lang/expr v1.17.6 // indirect + github.com/expr-lang/expr v1.17.7 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect diff --git a/go.sum b/go.sum index 3d2fb3e1..abe7903d 100644 --- a/go.sum +++ b/go.sum @@ -88,8 +88,8 @@ github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjT github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec= -github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= +github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8= +github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= From 6954485c7b1b93032a10852989b8570f5b7a49c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:37:45 -0700 Subject: [PATCH 29/49] chore(deps): bump github.com/containerd/containerd from 1.7.29 to 1.7.33 (#264) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 50a1475a..ebea0db3 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.3 // indirect - github.com/containerd/containerd v1.7.29 // indirect + github.com/containerd/containerd v1.7.33 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect diff --git a/go.sum b/go.sum index abe7903d..93c377a4 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE= -github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= +github.com/containerd/containerd v1.7.33 h1:iAkYGC/ifR/V+0eR4iXWHNGYUF0DF2PmGV5iz4Irj5M= +github.com/containerd/containerd v1.7.33/go.mod h1:gSbSCVjPCdkfJCjyrzz7aRC+xFlqVbatNpfHfVCYGUM= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= From b52159e6c2258c5a3aeb92055fc1f36a68f71465 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:37:58 +0000 Subject: [PATCH 30/49] chore(deps): bump github.com/go-playground/validator/v10 from 10.28.0 to 10.30.3 (#258) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index ebea0db3..dbd2ac7b 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/cybozu-go/moco v0.34.0 github.com/go-logr/logr v1.4.3 - github.com/go-playground/validator/v10 v10.28.0 + github.com/go-playground/validator/v10 v10.30.3 github.com/golang/glog v1.2.5 github.com/google/uuid v1.6.0 github.com/imdario/mergo v0.3.16 @@ -79,7 +79,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/go.sum b/go.sum index 93c377a4..f60e961f 100644 --- a/go.sum +++ b/go.sum @@ -102,8 +102,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= @@ -161,8 +161,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= -github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8= +github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= From 7759a8be10683e9573e118bbb06536d330e5de03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:42:54 -0700 Subject: [PATCH 31/49] chore(deps): bump oras.land/oras-go/v2 from 2.6.0 to 2.6.2 (#257) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index dbd2ac7b..8926ddcd 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( k8s.io/client-go v0.35.3 k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 knative.dev/pkg v0.0.0-20250326102644-9f3e60a9244c - oras.land/oras-go/v2 v2.6.0 + oras.land/oras-go/v2 v2.6.2 sigs.k8s.io/controller-runtime v0.22.4 sigs.k8s.io/gateway-api v1.5.0 sigs.k8s.io/yaml v1.6.0 @@ -175,7 +175,7 @@ require ( golang.org/x/mod v0.36.0 // indirect golang.org/x/net v0.56.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.21.0 // indirect + golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect golang.org/x/time v0.15.0 // indirect diff --git a/go.sum b/go.sum index f60e961f..7e843a43 100644 --- a/go.sum +++ b/go.sum @@ -451,8 +451,8 @@ golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= @@ -517,8 +517,8 @@ k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbe k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= knative.dev/pkg v0.0.0-20250326102644-9f3e60a9244c h1:6IZwH1QHGfWlmfdy7svgDCPhRqWpisWK/Gcp8wdAwE0= knative.dev/pkg v0.0.0-20250326102644-9f3e60a9244c/go.mod h1:gx7Pp9NPcKYApNhR8m0KSOeg71pqhwPWhuhUJ6xCa2g= -oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= -oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo= +oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= From 28f681a9482bec1bef2a00b5738fc41b9a84fd2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:32:10 -0700 Subject: [PATCH 32/49] chore(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.35.0 to 1.43.0 (#265) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 8926ddcd..95cd84a7 100644 --- a/go.mod +++ b/go.mod @@ -109,7 +109,7 @@ require ( github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -159,14 +159,14 @@ require ( go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect go.opentelemetry.io/otel/log v0.11.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect diff --git a/go.sum b/go.sum index 7e843a43..cd5062de 100644 --- a/go.sum +++ b/go.sum @@ -199,8 +199,8 @@ github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -403,12 +403,12 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcF go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= @@ -429,8 +429,8 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= From 295e17ac5b6d1698d745877fb6d34c2d26336a7e Mon Sep 17 00:00:00 2001 From: Casey Abernathy <cabernathy@coreweave.com> Date: Tue, 21 Jul 2026 14:40:40 -0700 Subject: [PATCH 33/49] feat(seaweed): Upgrade seaweed chart version, set readiness probe (#273) --- deploy/operator/Chart.lock | 6 +- deploy/operator/Chart.yaml | 2 +- .../managed/objectstore/seaweedfs/spec.go | 5 + .../objectstore/seaweedfs/spec_test.go | 18 + pkg/vendored/seaweedfs-operator/README.md | 5 +- .../crds/seaweed.seaweedfs.com_seaweeds.yaml | 3878 ++++++++++++++++- .../v1/groupversion_info.go | 1 + .../seaweed.seaweedfs.com/v1/types.go | 22 + .../v1/zz_generated.deepcopy.go | 89 +- 9 files changed, 3961 insertions(+), 65 deletions(-) diff --git a/deploy/operator/Chart.lock b/deploy/operator/Chart.lock index 5a1c1012..c97534c7 100644 --- a/deploy/operator/Chart.lock +++ b/deploy/operator/Chart.lock @@ -10,7 +10,7 @@ dependencies: version: 0.22.2 - name: seaweedfs-operator repository: https://seaweedfs.github.io/seaweedfs-operator/ - version: 0.1.24 + version: 0.1.35 - name: prometheus-operator-crds repository: https://prometheus-community.github.io/helm-charts version: 29.0.0 @@ -26,5 +26,5 @@ dependencies: - name: telemetry repository: file://../telemetry version: 0.1.0 -digest: sha256:c236896848dcfa23f87e476e0ea552ffb874e294e08e614a7908019e03ee68a1 -generated: "2026-07-08T16:05:34.522143-07:00" +digest: sha256:b03eebae8a867a43e00fe2d3bd3e89c2af552a0c998d0046c56865128090f123 +generated: "2026-07-17T13:39:19.593182-07:00" diff --git a/deploy/operator/Chart.yaml b/deploy/operator/Chart.yaml index a2f878a8..a38b8c3b 100644 --- a/deploy/operator/Chart.yaml +++ b/deploy/operator/Chart.yaml @@ -24,7 +24,7 @@ dependencies: repository: https://ot-container-kit.github.io/helm-charts condition: redis-operator.enabled - name: seaweedfs-operator - version: 0.1.24 + version: 0.1.35 repository: https://seaweedfs.github.io/seaweedfs-operator/ condition: seaweedfs-operator.enabled - name: prometheus-operator-crds diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/spec.go b/internal/controller/infra/managed/objectstore/seaweedfs/spec.go index e98b7436..d2bb9145 100644 --- a/internal/controller/infra/managed/objectstore/seaweedfs/spec.go +++ b/internal/controller/infra/managed/objectstore/seaweedfs/spec.go @@ -49,6 +49,8 @@ const ( seaweedVolumeMetricsPort int32 = 9092 seaweedFilerMetricsPort int32 = 9093 seaweedVolumeSizeLimitMB int64 = 1024 + + seaweedVolumeReadinessPeriodSeconds int32 = 15 ) func volumeLayout(storageQuantity resource.Quantity) (int32, int32) { @@ -155,6 +157,9 @@ func ToObjectStoreVendorSpec( Volumes: seaweedWritableVolumes(), VolumeMounts: seaweedWritableVolumeMounts(), ExtraArgs: []string{"-ip.bind=0.0.0.0"}, + ReadinessProbe: &seaweedv1.ProbeOverride{ + PeriodSeconds: ptr.To(seaweedVolumeReadinessPeriodSeconds), + }, }, // Operator sizes the data PVC from Requests[storage] — a persistent disk, not ephemeral. ResourceRequirements: corev1.ResourceRequirements{ diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/spec_test.go b/internal/controller/infra/managed/objectstore/seaweedfs/spec_test.go index d90161a6..770cb5cf 100644 --- a/internal/controller/infra/managed/objectstore/seaweedfs/spec_test.go +++ b/internal/controller/infra/managed/objectstore/seaweedfs/spec_test.go @@ -2,6 +2,7 @@ package seaweedfs import ( "context" + "encoding/json" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -141,6 +142,23 @@ var _ = Describe("SeaweedFS vendor specs", func() { Expect(*seaweed.Spec.Filer.MetricsPort).To(Equal(seaweedFilerMetricsPort)) }) + It("uses a fast readiness cadence for volume servers", func() { + wandb := seaweedWandb() + seaweed, err := ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(seaweed).NotTo(BeNil()) + + Expect(seaweed.Spec.Volume.ReadinessProbe).NotTo(BeNil()) + Expect(seaweed.Spec.Volume.ReadinessProbe.PeriodSeconds).NotTo(BeNil()) + Expect(*seaweed.Spec.Volume.ReadinessProbe.PeriodSeconds).To(Equal(seaweedVolumeReadinessPeriodSeconds)) + Expect(seaweed.Spec.Volume.LivenessProbe).To(BeNil()) + + encoded, err := json.Marshal(seaweed) + Expect(err).NotTo(HaveOccurred()) + Expect(string(encoded)).To(ContainSubstring(`"readinessProbe":{"periodSeconds":15}`)) + Expect(string(encoded)).NotTo(ContainSubstring(`"livenessProbe"`)) + }) + It("keeps the volume storage request when cpu/memory overrides are set", func() { wandb := seaweedWandb() seaweed, err := ToObjectStoreVendorSpec(context.Background(), wandb, wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ManagedObjectStore, seaweedScheme(), manifest.Manifest{}) diff --git a/pkg/vendored/seaweedfs-operator/README.md b/pkg/vendored/seaweedfs-operator/README.md index e0a84175..d5e9d2c7 100644 --- a/pkg/vendored/seaweedfs-operator/README.md +++ b/pkg/vendored/seaweedfs-operator/README.md @@ -5,8 +5,9 @@ This directory contains vendored API types from the [SeaweedFS Operator](https:/ ## Source - **Repository**: https://github.com/seaweedfs/seaweedfs-operator -- **Version**: v0.1.13 -- **Date Vendored**: 2026-04-13 +- **Operator Version**: 1.0.32 +- **Helm Chart Version**: 0.1.35 +- **Date Vendored**: 2026-07-17 ## Reason for Vendoring diff --git a/pkg/vendored/seaweedfs-operator/crds/seaweed.seaweedfs.com_seaweeds.yaml b/pkg/vendored/seaweedfs-operator/crds/seaweed.seaweedfs.com_seaweeds.yaml index 9b08316a..425796e5 100644 --- a/pkg/vendored/seaweedfs-operator/crds/seaweed.seaweedfs.com_seaweeds.yaml +++ b/pkg/vendored/seaweedfs-operator/crds/seaweed.seaweedfs.com_seaweeds.yaml @@ -473,6 +473,8 @@ spec: properties: name: type: string + request: + type: string required: - name type: object @@ -480,6 +482,76 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map + containerSecurityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object credentialsSecret: properties: name: @@ -519,6 +591,23 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: @@ -598,6 +687,12 @@ spec: type: object type: array type: object + initContainers: + x-kubernetes-preserve-unknown-fields: true + labels: + additionalProperties: + type: string + type: object limits: additionalProperties: anyOf: @@ -606,14 +701,127 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object + livenessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object + loggingArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic metricsPort: type: integer nodeSelector: additionalProperties: type: string type: object + podSecurityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object priorityClassName: type: string + readinessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + successThreshold: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object requests: additionalProperties: anyOf: @@ -637,6 +845,10 @@ spec: type: type: string type: object + serviceAccountName: + type: string + sidecars: + x-kubernetes-preserve-unknown-fields: true statefulSetUpdateStrategy: type: string terminationGracePeriodSeconds: @@ -705,10 +917,12 @@ spec: diskURI: type: string fsType: + default: ext4 type: string kind: type: string readOnly: + default: false type: boolean required: - diskName @@ -1062,6 +1276,13 @@ spec: required: - path type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object iscsi: properties: chapAuthDiscovery: @@ -1075,6 +1296,7 @@ spec: iqn: type: string iscsiInterface: + default: default type: string lun: type: integer @@ -1253,6 +1475,28 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object secret: properties: items: @@ -1317,6 +1561,7 @@ spec: image: type: string keyring: + default: /etc/ceph/keyring type: string monitors: items: @@ -1324,6 +1569,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd type: string readOnly: type: boolean @@ -1335,6 +1581,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin type: string required: - image @@ -1343,6 +1590,7 @@ spec: scaleIO: properties: fsType: + default: xfs type: string gateway: type: string @@ -1360,6 +1608,7 @@ spec: sslEnabled: type: boolean storageMode: + default: ThinProvisioned type: string storagePool: type: string @@ -1872,6 +2121,167 @@ spec: additionalProperties: type: string type: object + backup: + properties: + dataMirror: + items: + properties: + filerPath: + default: / + type: string + storageName: + maxLength: 50 + minLength: 1 + type: string + required: + - storageName + type: object + maxItems: 32 + type: array + x-kubernetes-list-map-keys: + - storageName + x-kubernetes-list-type: map + image: + type: string + schedule: + items: + properties: + filerPath: + default: / + type: string + keep: + minimum: 0 + type: integer + name: + maxLength: 50 + minLength: 1 + type: string + schedule: + maxLength: 120 + minLength: 1 + type: string + storageName: + maxLength: 50 + minLength: 1 + type: string + suspend: + default: false + type: boolean + required: + - name + - schedule + - storageName + type: object + maxItems: 32 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + storages: + additionalProperties: + properties: + azure: + properties: + accountName: + minLength: 1 + type: string + container: + minLength: 1 + type: string + directory: + default: / + type: string + required: + - accountName + - container + type: object + b2: + properties: + bucket: + minLength: 1 + type: string + directory: + default: / + type: string + region: + type: string + required: + - bucket + type: object + credentialsSecret: + type: string + filesystem: + properties: + existingClaim: + minLength: 1 + type: string + mountPath: + default: /backup + type: string + subPath: + type: string + required: + - existingClaim + type: object + gcs: + properties: + bucket: + minLength: 1 + type: string + directory: + default: / + type: string + required: + - bucket + type: object + s3: + properties: + bucket: + minLength: 1 + type: string + directory: + default: / + type: string + endpoint: + type: string + forcePathStyle: + default: true + type: boolean + region: + type: string + required: + - bucket + type: object + type: + enum: + - s3 + - gcs + - azure + - b2 + - filesystem + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: storage must set the sub-block matching its type + rule: (self.type != 's3' || has(self.s3)) && (self.type != + 'gcs' || has(self.gcs)) && (self.type != 'azure' || has(self.azure)) + && (self.type != 'b2' || has(self.b2)) && (self.type != + 'filesystem' || has(self.filesystem)) + maxProperties: 32 + minProperties: 1 + type: object + required: + - storages + type: object + x-kubernetes-validations: + - message: schedule.storageName must reference a defined storage + rule: '!has(self.schedule) || self.schedule.all(s, s.storageName + in self.storages)' + - message: dataMirror.storageName must reference a defined storage + rule: '!has(self.dataMirror) || self.dataMirror.all(m, m.storageName + in self.storages)' enablePVReclaim: type: boolean filer: @@ -2321,6 +2731,8 @@ spec: properties: name: type: string + request: + type: string required: - name type: object @@ -2330,6 +2742,76 @@ spec: x-kubernetes-list-type: map config: type: string + containerSecurityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object env: items: properties: @@ -2362,6 +2844,23 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: @@ -2401,6 +2900,33 @@ spec: type: string type: array x-kubernetes-list-type: atomic + grpcIngress: + properties: + annotations: + additionalProperties: + type: string + type: object + className: + type: string + enabled: + type: boolean + host: + type: string + path: + default: / + type: string + tls: + items: + properties: + hosts: + items: + type: string + type: array + secretName: + type: string + type: object + type: array + type: object hostNetwork: type: boolean iam: @@ -2454,6 +2980,12 @@ spec: type: object type: array type: object + initContainers: + x-kubernetes-preserve-unknown-fields: true + labels: + additionalProperties: + type: string + type: object limits: additionalProperties: anyOf: @@ -2462,6 +2994,26 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object + livenessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object + loggingArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic maxMB: type: integer metricsPort: @@ -2478,6 +3030,10 @@ spec: items: type: string type: array + annotations: + additionalProperties: + type: string + type: object dataSource: properties: apiGroup: @@ -2496,6 +3052,10 @@ spec: type: boolean existingClaim: type: string + labels: + additionalProperties: + type: string + type: object mountPath: default: /data type: string @@ -2557,8 +3117,101 @@ spec: volumeName: type: string type: object + podSecurityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object priorityClassName: type: string + readinessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + successThreshold: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object replicas: minimum: 1 type: integer @@ -2631,6 +3284,10 @@ spec: type: type: string type: object + serviceAccountName: + type: string + sidecars: + x-kubernetes-preserve-unknown-fields: true statefulSetUpdateStrategy: type: string terminationGracePeriodSeconds: @@ -2699,10 +3356,12 @@ spec: diskURI: type: string fsType: + default: ext4 type: string kind: type: string readOnly: + default: false type: boolean required: - diskName @@ -3056,6 +3715,13 @@ spec: required: - path type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object iscsi: properties: chapAuthDiscovery: @@ -3069,6 +3735,7 @@ spec: iqn: type: string iscsiInterface: + default: default type: string lun: type: integer @@ -3247,6 +3914,28 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object secret: properties: items: @@ -3311,6 +4000,7 @@ spec: image: type: string keyring: + default: /etc/ceph/keyring type: string monitors: items: @@ -3318,6 +4008,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd type: string readOnly: type: boolean @@ -3329,6 +4020,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin type: string required: - image @@ -3337,6 +4029,7 @@ spec: scaleIO: properties: fsType: + default: xfs type: string gateway: type: string @@ -3354,6 +4047,7 @@ spec: sslEnabled: type: boolean storageMode: + default: ThinProvisioned type: string storagePool: type: string @@ -3445,6 +4139,15 @@ spec: type: object x-kubernetes-map-type: atomic type: array + labels: + additionalProperties: + type: string + type: object + loggingArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic master: properties: affinity: @@ -3892,6 +4595,8 @@ spec: properties: name: type: string + request: + type: string required: - name type: object @@ -3903,6 +4608,76 @@ spec: type: boolean config: type: string + containerSecurityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object defaultReplication: type: string env: @@ -3937,6 +4712,23 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: @@ -3997,46 +4789,1851 @@ spec: additionalProperties: type: string type: object - className: - type: string - enabled: - type: boolean - host: - type: string - path: - default: / + className: + type: string + enabled: + type: boolean + host: + type: string + path: + default: / + type: string + tls: + items: + properties: + hosts: + items: + type: string + type: array + secretName: + type: string + type: object + type: array + type: object + initContainers: + x-kubernetes-preserve-unknown-fields: true + labels: + additionalProperties: + type: string + type: object + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + livenessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object + loggingArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + metricsPort: + type: integer + nodeSelector: + additionalProperties: + type: string + type: object + podSecurityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + priorityClassName: + type: string + pulseSeconds: + type: integer + readinessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + successThreshold: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object + replicas: + minimum: 1 + type: integer + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + schedulerName: + type: string + service: + properties: + annotations: + additionalProperties: + type: string + type: object + clusterIP: + type: string + loadBalancerIP: + type: string + type: + type: string + type: object + serviceAccountName: + type: string + sidecars: + x-kubernetes-preserve-unknown-fields: true + statefulSetUpdateStrategy: + type: string + terminationGracePeriodSeconds: + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + type: integer + value: + type: string + type: object + type: array + version: + type: string + volumeMounts: + items: + properties: + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + volumePreallocate: + type: boolean + volumeSizeLimitMB: + type: integer + volumes: + items: + properties: + awsElasticBlockStore: + properties: + fsType: + type: string + partition: + type: integer + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + azureDisk: + properties: + cachingMode: + type: string + diskName: + type: string + diskURI: + type: string + fsType: + default: ext4 + type: string + kind: + type: string + readOnly: + default: false + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + properties: + readOnly: + type: boolean + secretName: + type: string + shareName: + type: string + required: + - secretName + - shareName + type: object + cephfs: + properties: + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + path: + type: string + readOnly: + type: boolean + secretFile: + type: string + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + type: string + required: + - monitors + type: object + cinder: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeID: + type: string + required: + - volumeID + type: object + configMap: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + csi: + properties: + driver: + type: string + fsType: + type: string + nodePublishSecretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + readOnly: + type: boolean + volumeAttributes: + additionalProperties: + type: string + type: object + required: + - driver + type: object + downwardAPI: + properties: + defaultMode: + type: integer + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + properties: + volumeClaimTemplate: + properties: + metadata: + type: object + spec: + properties: + accessModes: + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + namespace: + type: string + required: + - kind + - name + type: object + resources: + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + selector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + type: string + volumeAttributesClassName: + type: string + volumeMode: + type: string + volumeName: + type: string + type: object + required: + - spec + type: object + type: object + fc: + properties: + fsType: + type: string + lun: + type: integer + readOnly: + type: boolean + targetWWNs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + wwids: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + flexVolume: + properties: + driver: + type: string + fsType: + type: string + options: + additionalProperties: + type: string + type: object + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + required: + - driver + type: object + flocker: + properties: + datasetName: + type: string + datasetUUID: + type: string + type: object + gcePersistentDisk: + properties: + fsType: + type: string + partition: + type: integer + pdName: + type: string + readOnly: + type: boolean + required: + - pdName + type: object + gitRepo: + properties: + directory: + type: string + repository: + type: string + revision: + type: string + required: + - repository + type: object + glusterfs: + properties: + endpoints: + type: string + path: + type: string + readOnly: + type: boolean + required: + - endpoints + - path + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object + iscsi: + properties: + chapAuthDiscovery: + type: boolean + chapAuthSession: + type: boolean + fsType: + type: string + initiatorName: + type: string + iqn: + type: string + iscsiInterface: + default: default + type: string + lun: + type: integer + portals: + items: + type: string + type: array + x-kubernetes-list-type: atomic + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + targetPortal: + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + type: string + nfs: + properties: + path: + type: string + readOnly: + type: boolean + server: + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + properties: + fsType: + type: string + pdID: + type: string + required: + - pdID + type: object + portworxVolume: + properties: + fsType: + type: string + readOnly: + type: boolean + volumeID: + type: string + required: + - volumeID + type: object + projected: + properties: + defaultMode: + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + quobyte: + properties: + group: + type: string + readOnly: + type: boolean + registry: + type: string + tenant: + type: string + user: + type: string + volume: + type: string + required: + - registry + - volume + type: object + rbd: + properties: + fsType: + type: string + image: + type: string + keyring: + default: /etc/ceph/keyring + type: string + monitors: + items: + type: string + type: array + x-kubernetes-list-type: atomic + pool: + default: rbd + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + user: + default: admin + type: string + required: + - image + - monitors + type: object + scaleIO: + properties: + fsType: + default: xfs + type: string + gateway: + type: string + protectionDomain: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + sslEnabled: + type: boolean + storageMode: + default: ThinProvisioned + type: string + storagePool: + type: string + system: + type: string + volumeName: + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + properties: + defaultMode: + type: integer + items: + items: + properties: + key: + type: string + mode: + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + storageos: + properties: + fsType: + type: string + readOnly: + type: boolean + secretRef: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + type: string + volumeNamespace: + type: string + type: object + vsphereVolume: + properties: + fsType: + type: string + storagePolicyID: + type: string + storagePolicyName: + type: string + volumePath: + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + required: + - replicas + type: object + metricsAddress: + type: string + nodeSelector: + additionalProperties: + type: string + type: object + pvReclaimPolicy: + type: string + s3: + properties: + affinity: + properties: + nodeAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + preference: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + properties: + nodeSelectorTerms: + items: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + properties: + preferredDuringSchedulingIgnoredDuringExecution: + items: + properties: + podAffinityTerm: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + weight: + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + items: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + annotations: + additionalProperties: + type: string + type: object + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + configSecret: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + containerSecurityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object + domainName: + type: string + env: + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + extraArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + hostNetwork: + type: boolean + iam: + default: true + type: boolean + imagePullPolicy: + type: string + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + ingress: + properties: + annotations: + additionalProperties: + type: string + type: object + className: + type: string + enabled: + type: boolean + host: + type: string + path: + default: / + type: string + tls: + items: + properties: + hosts: + items: + type: string + type: array + secretName: + type: string + type: object + type: array + type: object + initContainers: + x-kubernetes-preserve-unknown-fields: true + labels: + additionalProperties: + type: string + type: object + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + livenessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object + loggingArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + metricsPort: + type: integer + nodeSelector: + additionalProperties: + type: string + type: object + podSecurityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: type: string - tls: + sysctls: items: properties: - hosts: - items: - type: string - type: array - secretName: + name: type: string + value: + type: string + required: + - name + - value type: object type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object type: object - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - metricsPort: + port: type: integer - nodeSelector: - additionalProperties: - type: string - type: object priorityClassName: type: string - pulseSeconds: - type: integer + readinessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + successThreshold: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object replicas: + default: 1 minimum: 1 type: integer requests: @@ -4062,6 +6659,10 @@ spec: type: type: string type: object + serviceAccountName: + type: string + sidecars: + x-kubernetes-preserve-unknown-fields: true statefulSetUpdateStrategy: type: string terminationGracePeriodSeconds: @@ -4105,10 +6706,6 @@ spec: - name type: object type: array - volumePreallocate: - type: boolean - volumeSizeLimitMB: - type: integer volumes: items: properties: @@ -4134,10 +6731,12 @@ spec: diskURI: type: string fsType: + default: ext4 type: string kind: type: string readOnly: + default: false type: boolean required: - diskName @@ -4491,6 +7090,13 @@ spec: required: - path type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object iscsi: properties: chapAuthDiscovery: @@ -4504,6 +7110,7 @@ spec: iqn: type: string iscsiInterface: + default: default type: string lun: type: integer @@ -4682,6 +7289,28 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object secret: properties: items: @@ -4746,6 +7375,7 @@ spec: image: type: string keyring: + default: /etc/ceph/keyring type: string monitors: items: @@ -4753,6 +7383,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd type: string readOnly: type: boolean @@ -4764,6 +7395,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin type: string required: - image @@ -4772,6 +7404,7 @@ spec: scaleIO: properties: fsType: + default: xfs type: string gateway: type: string @@ -4789,6 +7422,7 @@ spec: sslEnabled: type: boolean storageMode: + default: ThinProvisioned type: string storagePool: type: string @@ -4863,15 +7497,9 @@ spec: required: - replicas type: object - metricsAddress: - type: string - nodeSelector: - additionalProperties: - type: string - type: object - pvReclaimPolicy: + schedulerName: type: string - s3: + sftp: properties: affinity: properties: @@ -5313,11 +7941,15 @@ spec: additionalProperties: type: string type: object + authMethods: + type: string claims: items: properties: name: type: string + request: + type: string required: - name type: object @@ -5325,21 +7957,76 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map - configSecret: + containerSecurityContext: properties: - key: - type: string - name: - default: "" + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: type: string - optional: + readOnlyRootFilesystem: type: boolean - required: - - key + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object type: object - x-kubernetes-map-type: atomic - domainName: - type: string env: items: properties: @@ -5372,6 +8059,23 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: @@ -5411,11 +8115,15 @@ spec: type: string type: array x-kubernetes-list-type: atomic + hostKeysSecret: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic hostNetwork: type: boolean - iam: - default: true - type: boolean imagePullPolicy: type: string imagePullSecrets: @@ -5454,6 +8162,12 @@ spec: type: object type: array type: object + initContainers: + x-kubernetes-preserve-unknown-fields: true + labels: + additionalProperties: + type: string + type: object limits: additionalProperties: anyOf: @@ -5462,16 +8176,134 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object + livenessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object + loggingArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxAuthTries: + minimum: 1 + type: integer metricsPort: type: integer nodeSelector: additionalProperties: type: string type: object + podSecurityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object port: + maximum: 65535 + minimum: 1 type: integer priorityClassName: type: string + readinessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + successThreshold: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object replicas: default: 1 minimum: 1 @@ -5499,6 +8331,10 @@ spec: type: type: string type: object + serviceAccountName: + type: string + sidecars: + x-kubernetes-preserve-unknown-fields: true statefulSetUpdateStrategy: type: string terminationGracePeriodSeconds: @@ -5518,6 +8354,19 @@ spec: type: string type: object type: array + userStoreSecret: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic version: type: string volumeMounts: @@ -5567,10 +8416,12 @@ spec: diskURI: type: string fsType: + default: ext4 type: string kind: type: string readOnly: + default: false type: boolean required: - diskName @@ -5924,6 +8775,13 @@ spec: required: - path type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object iscsi: properties: chapAuthDiscovery: @@ -5937,6 +8795,7 @@ spec: iqn: type: string iscsiInterface: + default: default type: string lun: type: integer @@ -6115,6 +8974,28 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object secret: properties: items: @@ -6179,6 +9060,7 @@ spec: image: type: string keyring: + default: /etc/ceph/keyring type: string monitors: items: @@ -6186,6 +9068,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd type: string readOnly: type: boolean @@ -6197,6 +9080,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin type: string required: - image @@ -6205,6 +9089,7 @@ spec: scaleIO: properties: fsType: + default: xfs type: string gateway: type: string @@ -6222,6 +9107,7 @@ spec: sslEnabled: type: boolean storageMode: + default: ThinProvisioned type: string storagePool: type: string @@ -6296,8 +9182,6 @@ spec: required: - replicas type: object - schedulerName: - type: string statefulSetUpdateStrategy: type: string tls: @@ -6785,6 +9669,8 @@ spec: properties: name: type: string + request: + type: string required: - name type: object @@ -6794,6 +9680,76 @@ spec: x-kubernetes-list-type: map compactionMBps: type: integer + containerSecurityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object dataCenter: type: string env: @@ -6828,6 +9784,23 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: @@ -6873,6 +9846,32 @@ spec: type: boolean hostNetwork: type: boolean + hostPath: + items: + properties: + maxVolumeCount: + minimum: 0 + type: integer + path: + minLength: 1 + type: string + type: + default: DirectoryOrCreate + enum: + - "" + - DirectoryOrCreate + - Directory + - FileOrCreate + - File + - Socket + - CharDevice + - BlockDevice + type: string + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic idleTimeout: type: integer imagePullPolicy: @@ -6913,6 +9912,18 @@ spec: type: object type: array type: object + initContainers: + x-kubernetes-preserve-unknown-fields: true + kind: + default: StatefulSet + enum: + - StatefulSet + - DaemonSet + type: string + labels: + additionalProperties: + type: string + type: object limits: additionalProperties: anyOf: @@ -6921,6 +9932,26 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object + livenessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object + loggingArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic maxVolumeCounts: type: integer metricsPort: @@ -6931,10 +9962,103 @@ spec: additionalProperties: type: string type: object + podSecurityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object priorityClassName: type: string rack: type: string + readinessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + successThreshold: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object replicas: minimum: 0 type: integer @@ -6961,10 +10085,48 @@ spec: type: type: string type: object + serviceAccountName: + type: string + sidecars: + x-kubernetes-preserve-unknown-fields: true statefulSetUpdateStrategy: type: string + storageAnnotations: + additionalProperties: + type: string + type: object storageClassName: type: string + storageLabels: + additionalProperties: + type: string + type: object + storageSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic terminationGracePeriodSeconds: type: integer tolerations: @@ -7031,10 +10193,12 @@ spec: diskURI: type: string fsType: + default: ext4 type: string kind: type: string readOnly: + default: false type: boolean required: - diskName @@ -7388,6 +10552,13 @@ spec: required: - path type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object iscsi: properties: chapAuthDiscovery: @@ -7401,6 +10572,7 @@ spec: iqn: type: string iscsiInterface: + default: default type: string lun: type: integer @@ -7579,6 +10751,28 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object secret: properties: items: @@ -7643,6 +10837,7 @@ spec: image: type: string keyring: + default: /etc/ceph/keyring type: string monitors: items: @@ -7650,6 +10845,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd type: string readOnly: type: boolean @@ -7661,6 +10857,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin type: string required: - image @@ -7669,6 +10866,7 @@ spec: scaleIO: properties: fsType: + default: xfs type: string gateway: type: string @@ -7686,6 +10884,7 @@ spec: sslEnabled: type: boolean storageMode: + default: ThinProvisioned type: string storagePool: type: string @@ -8210,6 +11409,8 @@ spec: properties: name: type: string + request: + type: string required: - name type: object @@ -8219,6 +11420,76 @@ spec: x-kubernetes-list-type: map compactionMBps: type: integer + containerSecurityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object dataCenter: type: string env: @@ -8253,6 +11524,23 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: @@ -8311,6 +11599,12 @@ spec: type: object x-kubernetes-map-type: atomic type: array + initContainers: + x-kubernetes-preserve-unknown-fields: true + labels: + additionalProperties: + type: string + type: object limits: additionalProperties: anyOf: @@ -8319,6 +11613,26 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object + livenessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object + loggingArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic maxVolumeCounts: type: integer metricsPort: @@ -8329,10 +11643,103 @@ spec: additionalProperties: type: string type: object + podSecurityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object priorityClassName: type: string rack: type: string + readinessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + successThreshold: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object replicas: minimum: 0 type: integer @@ -8359,10 +11766,48 @@ spec: type: type: string type: object + serviceAccountName: + type: string + sidecars: + x-kubernetes-preserve-unknown-fields: true statefulSetUpdateStrategy: type: string + storageAnnotations: + additionalProperties: + type: string + type: object storageClassName: type: string + storageLabels: + additionalProperties: + type: string + type: object + storageSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic terminationGracePeriodSeconds: type: integer tolerations: @@ -8429,10 +11874,12 @@ spec: diskURI: type: string fsType: + default: ext4 type: string kind: type: string readOnly: + default: false type: boolean required: - diskName @@ -8786,6 +12233,13 @@ spec: required: - path type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object iscsi: properties: chapAuthDiscovery: @@ -8799,6 +12253,7 @@ spec: iqn: type: string iscsiInterface: + default: default type: string lun: type: integer @@ -8977,6 +12432,28 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object secret: properties: items: @@ -9041,6 +12518,7 @@ spec: image: type: string keyring: + default: /etc/ceph/keyring type: string monitors: items: @@ -9048,6 +12526,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd type: string readOnly: type: boolean @@ -9059,6 +12538,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin type: string required: - image @@ -9067,6 +12547,7 @@ spec: scaleIO: properties: fsType: + default: xfs type: string gateway: type: string @@ -9084,6 +12565,7 @@ spec: sslEnabled: type: boolean storageMode: + default: ThinProvisioned type: string storagePool: type: string @@ -9608,6 +13090,8 @@ spec: properties: name: type: string + request: + type: string required: - name type: object @@ -9615,6 +13099,76 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map + containerSecurityContext: + properties: + allowPrivilegeEscalation: + type: boolean + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + capabilities: + properties: + add: + items: + type: string + type: array + x-kubernetes-list-type: atomic + drop: + items: + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + type: boolean + procMount: + type: string + readOnlyRootFilesystem: + type: boolean + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object env: items: properties: @@ -9647,6 +13201,23 @@ spec: - fieldPath type: object x-kubernetes-map-type: atomic + fileKeyRef: + properties: + key: + type: string + optional: + default: false + type: boolean + path: + type: string + volumeName: + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: @@ -9699,9 +13270,15 @@ spec: type: object x-kubernetes-map-type: atomic type: array + initContainers: + x-kubernetes-preserve-unknown-fields: true jobType: default: all type: string + labels: + additionalProperties: + type: string + type: object limits: additionalProperties: anyOf: @@ -9710,6 +13287,26 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object + livenessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object + loggingArgs: + items: + type: string + type: array + x-kubernetes-list-type: atomic maxDetect: minimum: 1 type: integer @@ -9730,6 +13327,10 @@ spec: items: type: string type: array + annotations: + additionalProperties: + type: string + type: object dataSource: properties: apiGroup: @@ -9748,6 +13349,10 @@ spec: type: boolean existingClaim: type: string + labels: + additionalProperties: + type: string + type: object mountPath: default: /data type: string @@ -9809,8 +13414,101 @@ spec: volumeName: type: string type: object + podSecurityContext: + properties: + appArmorProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + fsGroup: + type: integer + fsGroupChangePolicy: + type: string + runAsGroup: + type: integer + runAsNonRoot: + type: boolean + runAsUser: + type: integer + seLinuxChangePolicy: + type: string + seLinuxOptions: + properties: + level: + type: string + role: + type: string + type: + type: string + user: + type: string + type: object + seccompProfile: + properties: + localhostProfile: + type: string + type: + type: string + required: + - type + type: object + supplementalGroups: + items: + type: integer + type: array + x-kubernetes-list-type: atomic + supplementalGroupsPolicy: + type: string + sysctls: + items: + properties: + name: + type: string + value: + type: string + required: + - name + - value + type: object + type: array + x-kubernetes-list-type: atomic + windowsOptions: + properties: + gmsaCredentialSpec: + type: string + gmsaCredentialSpecName: + type: string + hostProcess: + type: boolean + runAsUserName: + type: string + type: object + type: object priorityClassName: type: string + readinessProbe: + properties: + failureThreshold: + minimum: 1 + type: integer + initialDelaySeconds: + minimum: 0 + type: integer + periodSeconds: + minimum: 1 + type: integer + successThreshold: + minimum: 1 + type: integer + timeoutSeconds: + minimum: 1 + type: integer + type: object replicas: default: 1 minimum: 1 @@ -9825,6 +13523,10 @@ spec: type: object schedulerName: type: string + serviceAccountName: + type: string + sidecars: + x-kubernetes-preserve-unknown-fields: true statefulSetUpdateStrategy: type: string terminationGracePeriodSeconds: @@ -9893,10 +13595,12 @@ spec: diskURI: type: string fsType: + default: ext4 type: string kind: type: string readOnly: + default: false type: boolean required: - diskName @@ -10250,6 +13954,13 @@ spec: required: - path type: object + image: + properties: + pullPolicy: + type: string + reference: + type: string + type: object iscsi: properties: chapAuthDiscovery: @@ -10263,6 +13974,7 @@ spec: iqn: type: string iscsiInterface: + default: default type: string lun: type: integer @@ -10441,6 +14153,28 @@ spec: type: array x-kubernetes-list-type: atomic type: object + podCertificate: + properties: + certificateChainPath: + type: string + credentialBundlePath: + type: string + keyPath: + type: string + keyType: + type: string + maxExpirationSeconds: + type: integer + signerName: + type: string + userAnnotations: + additionalProperties: + type: string + type: object + required: + - keyType + - signerName + type: object secret: properties: items: @@ -10505,6 +14239,7 @@ spec: image: type: string keyring: + default: /etc/ceph/keyring type: string monitors: items: @@ -10512,6 +14247,7 @@ spec: type: array x-kubernetes-list-type: atomic pool: + default: rbd type: string readOnly: type: boolean @@ -10523,6 +14259,7 @@ spec: type: object x-kubernetes-map-type: atomic user: + default: admin type: string required: - image @@ -10531,6 +14268,7 @@ spec: scaleIO: properties: fsType: + default: xfs type: string gateway: type: string @@ -10548,6 +14286,7 @@ spec: sslEnabled: type: boolean storageMode: + default: ThinProvisioned type: string storagePool: type: string @@ -10634,6 +14373,22 @@ spec: minimum: 0 type: integer type: object + backupMirrors: + items: + properties: + deploymentName: + type: string + ready: + type: boolean + storageName: + type: string + required: + - storageName + type: object + type: array + x-kubernetes-list-map-keys: + - storageName + x-kubernetes-list-type: map conditions: items: properties: @@ -10701,6 +14456,15 @@ spec: minimum: 0 type: integer type: object + sftp: + properties: + readyReplicas: + minimum: 0 + type: integer + replicas: + minimum: 0 + type: integer + type: object volume: properties: readyReplicas: diff --git a/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/groupversion_info.go b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/groupversion_info.go index ce077e8e..45227715 100644 --- a/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/groupversion_info.go +++ b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/groupversion_info.go @@ -12,6 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package v1 contains API Schema definitions for the seaweed v1 API group. package v1 import ( diff --git a/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/types.go b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/types.go index f8222941..fcea5928 100644 --- a/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/types.go +++ b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/types.go @@ -242,6 +242,26 @@ type WorkerSpec struct { MaxExecute *int32 `json:"maxExecute,omitempty"` } +// ProbeOverride tunes the timing fields of an operator-managed readiness probe. +// Nil fields retain the SeaweedFS operator defaults; the probe handler itself +// remains managed by the SeaweedFS operator. +type ProbeOverride struct { + InitialDelaySeconds *int32 `json:"initialDelaySeconds,omitempty"` + TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` + PeriodSeconds *int32 `json:"periodSeconds,omitempty"` + SuccessThreshold *int32 `json:"successThreshold,omitempty"` + FailureThreshold *int32 `json:"failureThreshold,omitempty"` +} + +// LivenessProbeOverride is ProbeOverride without SuccessThreshold, which +// Kubernetes requires to remain 1 for liveness probes. +type LivenessProbeOverride struct { + InitialDelaySeconds *int32 `json:"initialDelaySeconds,omitempty"` + TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` + PeriodSeconds *int32 `json:"periodSeconds,omitempty"` + FailureThreshold *int32 `json:"failureThreshold,omitempty"` +} + // ComponentSpec is the base spec of each component type ComponentSpec struct { Version *string `json:"version,omitempty"` @@ -260,6 +280,8 @@ type ComponentSpec struct { Volumes []corev1.Volume `json:"volumes,omitempty"` VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` ExtraArgs []string `json:"extraArgs,omitempty"` + ReadinessProbe *ProbeOverride `json:"readinessProbe,omitempty"` + LivenessProbe *LivenessProbeOverride `json:"livenessProbe,omitempty"` } // ServiceSpec is a subset of the original k8s spec diff --git a/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/zz_generated.deepcopy.go b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/zz_generated.deepcopy.go index b9d323a9..2342eb7b 100644 --- a/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/zz_generated.deepcopy.go +++ b/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1/zz_generated.deepcopy.go @@ -1,7 +1,7 @@ //go:build !ignore_autogenerated /* - +Copyright 2025. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ package v1 import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -153,6 +153,16 @@ func (in *ComponentSpec) DeepCopyInto(out *ComponentSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.ReadinessProbe != nil { + in, out := &in.ReadinessProbe, &out.ReadinessProbe + *out = new(ProbeOverride) + (*in).DeepCopyInto(*out) + } + if in.LivenessProbe != nil { + in, out := &in.LivenessProbe, &out.LivenessProbe + *out = new(LivenessProbeOverride) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentSpec. @@ -316,6 +326,41 @@ func (in *IngressTLS) DeepCopy() *IngressTLS { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LivenessProbeOverride) DeepCopyInto(out *LivenessProbeOverride) { + *out = *in + if in.InitialDelaySeconds != nil { + in, out := &in.InitialDelaySeconds, &out.InitialDelaySeconds + *out = new(int32) + **out = **in + } + if in.TimeoutSeconds != nil { + in, out := &in.TimeoutSeconds, &out.TimeoutSeconds + *out = new(int32) + **out = **in + } + if in.PeriodSeconds != nil { + in, out := &in.PeriodSeconds, &out.PeriodSeconds + *out = new(int32) + **out = **in + } + if in.FailureThreshold != nil { + in, out := &in.FailureThreshold, &out.FailureThreshold + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LivenessProbeOverride. +func (in *LivenessProbeOverride) DeepCopy() *LivenessProbeOverride { + if in == nil { + return nil + } + out := new(LivenessProbeOverride) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MasterSpec) DeepCopyInto(out *MasterSpec) { *out = *in @@ -439,6 +484,46 @@ func (in *PersistenceSpec) DeepCopy() *PersistenceSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProbeOverride) DeepCopyInto(out *ProbeOverride) { + *out = *in + if in.InitialDelaySeconds != nil { + in, out := &in.InitialDelaySeconds, &out.InitialDelaySeconds + *out = new(int32) + **out = **in + } + if in.TimeoutSeconds != nil { + in, out := &in.TimeoutSeconds, &out.TimeoutSeconds + *out = new(int32) + **out = **in + } + if in.PeriodSeconds != nil { + in, out := &in.PeriodSeconds, &out.PeriodSeconds + *out = new(int32) + **out = **in + } + if in.SuccessThreshold != nil { + in, out := &in.SuccessThreshold, &out.SuccessThreshold + *out = new(int32) + **out = **in + } + if in.FailureThreshold != nil { + in, out := &in.FailureThreshold, &out.FailureThreshold + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProbeOverride. +func (in *ProbeOverride) DeepCopy() *ProbeOverride { + if in == nil { + return nil + } + out := new(ProbeOverride) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *S3Config) DeepCopyInto(out *S3Config) { *out = *in From 8f77f6dfe5de7ecfdc9fab503c9d3b50d62df438 Mon Sep 17 00:00:00 2001 From: Daniel Panzella <daniel.panzella@wandb.com> Date: Wed, 22 Jul 2026 08:54:30 -0700 Subject: [PATCH 34/49] fix: Emit FQDN for manifest service-source URLs (#283) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> --- internal/controller/reconciler/pods.go | 10 ++++++++-- internal/controller/reconciler/telemetry_test.go | 4 ++-- pkg/wandb/manifest/manifest.go | 16 ++++++++++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/internal/controller/reconciler/pods.go b/internal/controller/reconciler/pods.go index 0bc6515f..ec03d584 100644 --- a/internal/controller/reconciler/pods.go +++ b/internal/controller/reconciler/pods.go @@ -368,7 +368,7 @@ func resolveEnvvars(ctx context.Context, client ctrlClient.Client, wandb *v2.Wei // applications status and reading from there is probably more correct // Prefer deterministic manifest-derived service resolution to avoid startup races // where the Service object has not been created yet. - if resolved, ok := manifest.ResolveServiceURL(src); ok { + if resolved, ok := manifest.ResolveServiceURL(src, wandb.Namespace); ok { components = append(components, resolved) continue } @@ -402,7 +402,13 @@ func resolveEnvvars(ctx context.Context, client ctrlClient.Client, wandb *v2.Wei } } } - components = append(components, fmt.Sprintf("%s%s:%d%s", proto, serviceList.Items[0].Name, selectedPort, src.Path)) + // Fully-qualified host (see ResolveServiceURL) so NO_PROXY cluster + // suffixes cover it; the Service was just listed InNamespace(wandb.Namespace). + svcHost := serviceList.Items[0].Name + if wandb.Namespace != "" { + svcHost = fmt.Sprintf("%s.%s.svc.cluster.local", serviceList.Items[0].Name, wandb.Namespace) + } + components = append(components, fmt.Sprintf("%s%s:%d%s", proto, svcHost, selectedPort, src.Path)) case "jwt-issuer-map": if wandb.Spec.Wandb.InternalServiceAuth.Enabled != nil && *wandb.Spec.Wandb.InternalServiceAuth.Enabled { diff --git a/internal/controller/reconciler/telemetry_test.go b/internal/controller/reconciler/telemetry_test.go index f10a9968..6ffb602b 100644 --- a/internal/controller/reconciler/telemetry_test.go +++ b/internal/controller/reconciler/telemetry_test.go @@ -953,7 +953,7 @@ func TestResolveEnvvarsServiceSourceFromManifest(t *testing.T) { } sweepProvider := mustFindEnvVar(t, resolved, "GORILLA_SWEEP_PROVIDER") - if sweepProvider.Value != "http://anaconda2:8080" { + if sweepProvider.Value != "http://anaconda2.default.svc.cluster.local:8080" { t.Fatalf("unexpected sweep provider value: %s", sweepProvider.Value) } } @@ -994,7 +994,7 @@ func TestResolveEnvvarsServiceSourcePortNameFromManifest(t *testing.T) { } historyStore := mustFindEnvVar(t, resolved, "GORILLA_HISTORY_STORE") - if historyStore.Value != "http://parquet:9000/_goRPC_" { + if historyStore.Value != "http://parquet.default.svc.cluster.local:9000/_goRPC_" { t.Fatalf("unexpected history store value: %s", historyStore.Value) } } diff --git a/pkg/wandb/manifest/manifest.go b/pkg/wandb/manifest/manifest.go index d25fdb25..1cf06bd7 100644 --- a/pkg/wandb/manifest/manifest.go +++ b/pkg/wandb/manifest/manifest.go @@ -781,7 +781,7 @@ func (m *Manifest) FeaturesEnabled(topicFeatures []string) bool { return false } -func (m *Manifest) ResolveServiceURL(src EnvSource) (string, bool) { +func (m *Manifest) ResolveServiceURL(src EnvSource, namespace string) (string, bool) { if src.Name == "" { return "", false } @@ -800,7 +800,19 @@ func (m *Manifest) ResolveServiceURL(src EnvSource) (string, bool) { if src.Proto != "" { protoPrefix = fmt.Sprintf("%s://", src.Proto) } - return fmt.Sprintf("%s%s:%d%s", protoPrefix, src.Name, port, src.Path), true + // Emit the fully-qualified service host (<name>.<namespace>.svc.cluster.local) + // rather than the bare service name. A bare single-label host only resolves + // via the consuming pod's DNS search domain, and — critically — is not + // matched by NO_PROXY suffix rules (.svc/.svc.cluster.local), so when a proxy + // is configured these internal service-to-service calls hairpin through it. + // The FQDN resolves unambiguously and is covered by the standard cluster-DNS + // NO_PROXY suffixes. Matches the FQDN convention the managed-infra reconcilers + // already use for datastore hosts. + host := src.Name + if namespace != "" { + host = fmt.Sprintf("%s.%s.svc.cluster.local", src.Name, namespace) + } + return fmt.Sprintf("%s%s:%d%s", protoPrefix, host, port, src.Path), true } func (a *Application) ResolveServicePortFromManifest(requestedPort string) (int32, bool) { From 715b40aa472b1c7892462ec50c2e0e5421e15f01 Mon Sep 17 00:00:00 2001 From: Daniel Panzella <daniel.panzella@wandb.com> Date: Wed, 22 Jul 2026 08:54:31 -0700 Subject: [PATCH 35/49] feat: First class support for http proxy configs (#284) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> --- api/v2/weightsandbiases_types.go | 50 ++++++ api/v2/zz_generated.deepcopy.go | 75 +++++++++ .../apps.wandb.com_weightsandbiases.yaml | 49 ++++++ internal/controller/reconciler/proxy_env.go | 113 +++++++++++++ .../controller/reconciler/proxy_env_test.go | 154 ++++++++++++++++++ .../controller/reconciler/reconcile_v2.go | 8 + .../apps.wandb.com_weightsandbiases.yaml | 49 ++++++ .../webhook/v2/weightsandbiases_proxy_test.go | 54 ++++++ .../webhook/v2/weightsandbiases_webhook.go | 63 +++++++ 9 files changed, 615 insertions(+) create mode 100644 internal/controller/reconciler/proxy_env.go create mode 100644 internal/controller/reconciler/proxy_env_test.go create mode 100644 internal/webhook/v2/weightsandbiases_proxy_test.go diff --git a/api/v2/weightsandbiases_types.go b/api/v2/weightsandbiases_types.go index 9ecc0274..b561a1d5 100644 --- a/api/v2/weightsandbiases_types.go +++ b/api/v2/weightsandbiases_types.go @@ -156,6 +156,56 @@ type GlobalSpec struct { // update tooling can discover them. // +optional CACertsConfigMap string `json:"caCertsConfigMap,omitempty"` + + // Proxy configures the forward-proxy egress settings injected into the + // application workloads (app Deployments, their init containers, and + // migration Jobs). The operator emits HTTP_PROXY/HTTPS_PROXY/NO_PROXY and + // their lowercase variants; NO_PROXY is always the operator-computed + // in-cluster exclusions merged over the user-supplied noProxy entries, so + // in-cluster datastore/service traffic never hairpins through the proxy. + // Pair with CustomCACerts for a TLS-intercepting proxy. + // +optional + Proxy *ProxySpec `json:"proxy,omitempty"` +} + +// ProxySpec is the forward-proxy configuration under spec.global.proxy. +type ProxySpec struct { + // HTTPProxy is the proxy URL for plain HTTP egress (HTTP_PROXY/http_proxy). + // +optional + HTTPProxy *ProxyValue `json:"httpProxy,omitempty"` + + // HTTPSProxy is the proxy URL for HTTPS egress (HTTPS_PROXY/https_proxy). + // +optional + HTTPSProxy *ProxyValue `json:"httpsProxy,omitempty"` + + // NoProxy holds EXTRA no-proxy entries appended to the operator-computed + // in-cluster exclusions. Use it for external endpoints (e.g. a BYOB object + // store) that must bypass the proxy. Entries must be comma-free; the + // operator owns the join. + // +optional + NoProxy []string `json:"noProxy,omitempty"` +} + +// ProxyValue is a value-or-secret union mirroring corev1.EnvVar semantics: +// exactly one of Value or ValueFrom must be set. Credential-bearing proxy URLs +// (http://user:pass@host:port) MUST use ValueFrom; the webhook rejects userinfo +// in a literal Value so credentials never land in the CR / etcd / kubectl output. +type ProxyValue struct { + // Value is a literal proxy URL. Must not contain userinfo (credentials). + // +optional + Value string `json:"value,omitempty"` + + // ValueFrom sources the proxy URL from a Secret key (may embed credentials). + // +optional + ValueFrom *ProxyValueSource `json:"valueFrom,omitempty"` +} + +// ProxyValueSource mirrors corev1.EnvVarSource (the secret case): the proxy URL +// is read from a Secret key. +type ProxyValueSource struct { + // SecretKeyRef selects a key of a Secret in the W&B namespace. + // +optional + SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef,omitempty"` } type NetworkingMode string diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index 30864877..2ee62bf9 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -489,6 +489,11 @@ func (in *GlobalSpec) DeepCopyInto(out *GlobalSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Proxy != nil { + in, out := &in.Proxy, &out.Proxy + *out = new(ProxySpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GlobalSpec. @@ -1124,6 +1129,76 @@ func (in *OidcSpec) DeepCopy() *OidcSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProxySpec) DeepCopyInto(out *ProxySpec) { + *out = *in + if in.HTTPProxy != nil { + in, out := &in.HTTPProxy, &out.HTTPProxy + *out = new(ProxyValue) + (*in).DeepCopyInto(*out) + } + if in.HTTPSProxy != nil { + in, out := &in.HTTPSProxy, &out.HTTPSProxy + *out = new(ProxyValue) + (*in).DeepCopyInto(*out) + } + if in.NoProxy != nil { + in, out := &in.NoProxy, &out.NoProxy + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxySpec. +func (in *ProxySpec) DeepCopy() *ProxySpec { + if in == nil { + return nil + } + out := new(ProxySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProxyValue) DeepCopyInto(out *ProxyValue) { + *out = *in + if in.ValueFrom != nil { + in, out := &in.ValueFrom, &out.ValueFrom + *out = new(ProxyValueSource) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyValue. +func (in *ProxyValue) DeepCopy() *ProxyValue { + if in == nil { + return nil + } + out := new(ProxyValue) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProxyValueSource) DeepCopyInto(out *ProxyValueSource) { + *out = *in + if in.SecretKeyRef != nil { + in, out := &in.SecretKeyRef, &out.SecretKeyRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyValueSource. +func (in *ProxyValueSource) DeepCopy() *ProxyValueSource { + if in == nil { + return nil + } + out := new(ProxyValueSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RedisConfig) DeepCopyInto(out *RedisConfig) { *out = *in diff --git a/config/crd/bases/apps.wandb.com_weightsandbiases.yaml b/config/crd/bases/apps.wandb.com_weightsandbiases.yaml index 5b5fa8d2..09deb2e0 100644 --- a/config/crd/bases/apps.wandb.com_weightsandbiases.yaml +++ b/config/crd/bases/apps.wandb.com_weightsandbiases.yaml @@ -1198,6 +1198,55 @@ spec: type: array imageRegistry: type: string + proxy: + properties: + httpProxy: + properties: + value: + type: string + valueFrom: + properties: + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object + httpsProxy: + properties: + value: + type: string + valueFrom: + properties: + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object + noProxy: + items: + type: string + type: array + type: object type: object kafka: properties: diff --git a/internal/controller/reconciler/proxy_env.go b/internal/controller/reconciler/proxy_env.go new file mode 100644 index 00000000..440132ac --- /dev/null +++ b/internal/controller/reconciler/proxy_env.go @@ -0,0 +1,113 @@ +package reconciler + +import ( + "os" + "strings" + + corev1 "k8s.io/api/core/v1" + + apiv2 "github.com/wandb/operator/api/v2" +) + +// proxyNoProxyStatic is the always-present in-cluster NO_PROXY baseline. Both +// .svc and the full suffix are emitted because suffix-match rules differ across +// stacks (Go matches name+subdomains; Python urllib does plain string-suffix). +// Datastore and app service hosts are NOT enumerated here: the operator emits +// FQDNs (<name>.<ns>.svc.cluster.local) for every wired service, so the +// .svc.cluster.local suffix already covers the whole in-cluster HTTP mesh. +var proxyNoProxyStatic = []string{ + "localhost", "127.0.0.1", "::1", + ".svc", ".svc.cluster.local", ".cluster.local", + "kubernetes.default.svc", +} + +// computeNoProxy builds the NO_PROXY value the app workloads receive: the static +// in-cluster baseline plus the API-server ClusterIP (an IP literal no dot-suffix +// rule covers) plus the user's extra entries, deduplicated with order preserved. +// The API-server IP comes from the operator pod's own $KUBERNETES_SERVICE_HOST, +// which is the same in-cluster API endpoint every app pod sees. +func computeNoProxy(extras []string) string { + entries := append([]string{}, proxyNoProxyStatic...) + if apiHost := os.Getenv("KUBERNETES_SERVICE_HOST"); apiHost != "" { + entries = append(entries, apiHost) + } + for _, e := range extras { + if e != "" { + entries = append(entries, e) + } + } + return joinNoProxy(entries) +} + +// joinNoProxy deduplicates entries (order preserved) and comma-joins them. +func joinNoProxy(entries []string) string { + seen := map[string]struct{}{} + out := make([]string, 0, len(entries)) + for _, e := range entries { + if _, ok := seen[e]; ok { + continue + } + seen[e] = struct{}{} + out = append(out, e) + } + return strings.Join(out, ",") +} + +// proxyValueEnvVars turns one ProxyValue into the upper/lower env-var pair for +// the given base name. A literal value becomes a literal env var; a valueFrom +// becomes a SecretKeyRef env source (both casings reference the same key) so +// credential-bearing URLs stay in the Secret and never land in the workload +// spec. Returns nil when the value is unset. +func proxyValueEnvVars(upper, lower string, pv *apiv2.ProxyValue) []corev1.EnvVar { + if pv == nil { + return nil + } + switch { + case pv.Value != "": + return []corev1.EnvVar{ + {Name: upper, Value: pv.Value}, + {Name: lower, Value: pv.Value}, + } + case pv.ValueFrom != nil && pv.ValueFrom.SecretKeyRef != nil: + return []corev1.EnvVar{ + {Name: upper, ValueFrom: &corev1.EnvVarSource{SecretKeyRef: pv.ValueFrom.SecretKeyRef.DeepCopy()}}, + {Name: lower, ValueFrom: &corev1.EnvVarSource{SecretKeyRef: pv.ValueFrom.SecretKeyRef.DeepCopy()}}, + } + default: + return nil + } +} + +// proxyEnvVars builds the full six-variable proxy env set for spec.global.proxy: +// HTTP_PROXY/HTTPS_PROXY plus the operator-computed NO_PROXY, and each one's +// lowercase twin (Go honors both casings; many libraries read only lowercase). +// NO_PROXY is emitted whenever any proxy URL is set — the computed baseline is +// never empty. +func proxyEnvVars(proxy *apiv2.ProxySpec) []corev1.EnvVar { + if proxy == nil { + return nil + } + var envVars []corev1.EnvVar + envVars = append(envVars, proxyValueEnvVars("HTTP_PROXY", "http_proxy", proxy.HTTPProxy)...) + envVars = append(envVars, proxyValueEnvVars("HTTPS_PROXY", "https_proxy", proxy.HTTPSProxy)...) + if proxy.HTTPProxy != nil || proxy.HTTPSProxy != nil { + noProxy := computeNoProxy(proxy.NoProxy) + envVars = append(envVars, + corev1.EnvVar{Name: "NO_PROXY", Value: noProxy}, + corev1.EnvVar{Name: "no_proxy", Value: noProxy}, + ) + } + return envVars +} + +// applyProxyToWorkload appends the spec.global.proxy env vars to a workload's +// env, skipping any name already present. Injected AFTER customCACerts env and +// BEFORE applyLegacyOverrideEnv, so a legacyOverrides entry can still override +// or blank any proxy var per-app (the deliberate escape hatch). No-op when +// spec.global.proxy is unset. +func applyProxyToWorkload(wandb *apiv2.WeightsAndBiases, envVars []corev1.EnvVar) []corev1.EnvVar { + if wandb.Spec.Global.Proxy == nil { + return envVars + } + return appendMissingEnvVars(envVars, proxyEnvVars(wandb.Spec.Global.Proxy)) +} diff --git a/internal/controller/reconciler/proxy_env_test.go b/internal/controller/reconciler/proxy_env_test.go new file mode 100644 index 00000000..06d16e44 --- /dev/null +++ b/internal/controller/reconciler/proxy_env_test.go @@ -0,0 +1,154 @@ +package reconciler + +import ( + "strings" + "testing" + + corev1 "k8s.io/api/core/v1" + + apiv2 "github.com/wandb/operator/api/v2" +) + +func envByName(vars []corev1.EnvVar, name string) (corev1.EnvVar, bool) { + for _, v := range vars { + if v.Name == name { + return v, true + } + } + return corev1.EnvVar{}, false +} + +func TestComputeNoProxyBaselineAndExtras(t *testing.T) { + t.Setenv("KUBERNETES_SERVICE_HOST", "10.96.0.1") + got := computeNoProxy([]string{"internal.example.com", "127.0.0.1"}) + parts := strings.Split(got, ",") + for _, want := range []string{ + "localhost", "127.0.0.1", "::1", ".svc", ".svc.cluster.local", + ".cluster.local", "kubernetes.default.svc", "10.96.0.1", "internal.example.com", + } { + found := false + for _, p := range parts { + if p == want { + found = true + } + } + if !found { + t.Errorf("NO_PROXY missing %q: %s", want, got) + } + } + // 127.0.0.1 appears once despite being both baseline and an extra. + count := 0 + for _, p := range parts { + if p == "127.0.0.1" { + count++ + } + } + if count != 1 { + t.Errorf("127.0.0.1 duplicated (%d): %s", count, got) + } +} + +func TestComputeNoProxyNoAPIServerHost(t *testing.T) { + t.Setenv("KUBERNETES_SERVICE_HOST", "") + got := computeNoProxy(nil) + if strings.Contains(got, ",,") || strings.HasPrefix(got, ",") || strings.HasSuffix(got, ",") { + t.Errorf("blank entry in NO_PROXY: %q", got) + } + if !strings.Contains(got, ".svc.cluster.local") { + t.Errorf("baseline missing suffix: %q", got) + } +} + +func TestProxyEnvVarsLiteral(t *testing.T) { + t.Setenv("KUBERNETES_SERVICE_HOST", "10.96.0.1") + proxy := &apiv2.ProxySpec{ + HTTPProxy: &apiv2.ProxyValue{Value: "http://proxy:3128"}, + HTTPSProxy: &apiv2.ProxyValue{Value: "http://proxy:3128"}, + NoProxy: []string{"wandb.localhost"}, + } + vars := proxyEnvVars(proxy) + // Six vars: HTTP_PROXY/HTTPS_PROXY/NO_PROXY + lowercase. + for _, name := range []string{"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"} { + v, ok := envByName(vars, name) + if !ok { + t.Fatalf("missing env var %q", name) + } + if v.ValueFrom != nil { + t.Errorf("%s should be a literal, got ValueFrom", name) + } + } + if v, _ := envByName(vars, "HTTP_PROXY"); v.Value != "http://proxy:3128" { + t.Errorf("HTTP_PROXY = %q", v.Value) + } + np, _ := envByName(vars, "NO_PROXY") + if !strings.Contains(np.Value, "wandb.localhost") || !strings.Contains(np.Value, "10.96.0.1") || !strings.Contains(np.Value, ".svc.cluster.local") { + t.Errorf("NO_PROXY missing computed baseline/extras: %q", np.Value) + } +} + +func TestProxyEnvVarsValueFromStaysSecretRef(t *testing.T) { + proxy := &apiv2.ProxySpec{ + HTTPSProxy: &apiv2.ProxyValue{ + ValueFrom: &apiv2.ProxyValueSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "egress-proxy"}, + Key: "httpsProxy", + }, + }, + }, + } + vars := proxyEnvVars(proxy) + // httpsProxy only: no HTTP_PROXY pair, but NO_PROXY still emitted. + if _, ok := envByName(vars, "HTTP_PROXY"); ok { + t.Errorf("HTTP_PROXY should be absent when only httpsProxy is set") + } + for _, name := range []string{"HTTPS_PROXY", "https_proxy"} { + v, ok := envByName(vars, name) + if !ok { + t.Fatalf("missing %q", name) + } + // Credential-bearing values stay a SecretKeyRef, never a literal. + if v.Value != "" || v.ValueFrom == nil || v.ValueFrom.SecretKeyRef == nil { + t.Errorf("%s should be a SecretKeyRef, got %+v", name, v) + } + if v.ValueFrom.SecretKeyRef.Name != "egress-proxy" || v.ValueFrom.SecretKeyRef.Key != "httpsProxy" { + t.Errorf("%s secret ref wrong: %+v", name, v.ValueFrom.SecretKeyRef) + } + } + if _, ok := envByName(vars, "NO_PROXY"); !ok { + t.Errorf("NO_PROXY should be emitted whenever any proxy URL is set") + } +} + +func TestProxyEnvVarsNil(t *testing.T) { + if proxyEnvVars(nil) != nil { + t.Errorf("nil proxy should yield no env vars") + } + // A ProxySpec with no URLs set emits nothing (NO_PROXY only rides with a proxy). + if got := proxyEnvVars(&apiv2.ProxySpec{}); len(got) != 0 { + t.Errorf("empty proxy spec should yield no env vars, got %v", got) + } +} + +func TestApplyProxyToWorkload(t *testing.T) { + t.Setenv("KUBERNETES_SERVICE_HOST", "10.96.0.1") + base := []corev1.EnvVar{{Name: "EXISTING", Value: "x"}, {Name: "HTTP_PROXY", Value: "manifest-value"}} + + // No proxy spec: unchanged. + wandbNoProxy := &apiv2.WeightsAndBiases{} + if got := applyProxyToWorkload(wandbNoProxy, base); len(got) != len(base) { + t.Fatalf("no-proxy workload should be unchanged, got %v", got) + } + + // With proxy: appends missing vars, does not clobber an existing HTTP_PROXY + // (appendMissing semantics — legacy/manifest precedence handled elsewhere). + wandb := &apiv2.WeightsAndBiases{} + wandb.Spec.Global.Proxy = &apiv2.ProxySpec{HTTPProxy: &apiv2.ProxyValue{Value: "http://proxy:3128"}} + got := applyProxyToWorkload(wandb, base) + if v, _ := envByName(got, "HTTP_PROXY"); v.Value != "manifest-value" { + t.Errorf("existing HTTP_PROXY should be preserved by appendMissing, got %q", v.Value) + } + if _, ok := envByName(got, "NO_PROXY"); !ok { + t.Errorf("NO_PROXY should have been appended") + } +} diff --git a/internal/controller/reconciler/reconcile_v2.go b/internal/controller/reconciler/reconcile_v2.go index e541c36a..38234bac 100644 --- a/internal/controller/reconciler/reconcile_v2.go +++ b/internal/controller/reconciler/reconcile_v2.go @@ -597,6 +597,10 @@ func reconcileApplications( return ctrl.Result{}, err } + // spec.global.proxy env: after CA (so both are present) and before legacy + // overrides (so legacyOverrides can still override/blank any proxy var). + envVars = applyProxyToWorkload(wandb, envVars) + // Applied last so legacy overrides beat manifest and injected env, as in v1. envVars = applyLegacyOverrideEnv(ctx, wandb, app.Name, envVars) @@ -1217,6 +1221,10 @@ func runMigrations(ctx context.Context, client ctrlClient.Client, wandb *apiv2.W return ctrl.Result{}, err } + // spec.global.proxy env (migration Jobs egress too — v1 parity); before + // legacy overrides so the escape hatch still wins. + envVars = applyProxyToWorkload(wandb, envVars) + // v1's global env reached job pods too (e.g. HTTP_PROXY); per-app entries don't apply here. envVars = overrideEnvVars(ctx, envVars, wandb.Spec.Wandb.LegacyOverrides[apiv2.LegacyOverridesGlobalKey].Env) diff --git a/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml b/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml index 5b5fa8d2..09deb2e0 100644 --- a/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml +++ b/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml @@ -1198,6 +1198,55 @@ spec: type: array imageRegistry: type: string + proxy: + properties: + httpProxy: + properties: + value: + type: string + valueFrom: + properties: + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object + httpsProxy: + properties: + value: + type: string + valueFrom: + properties: + secretKeyRef: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object + noProxy: + items: + type: string + type: array + type: object type: object kafka: properties: diff --git a/internal/webhook/v2/weightsandbiases_proxy_test.go b/internal/webhook/v2/weightsandbiases_proxy_test.go new file mode 100644 index 00000000..8a7f4162 --- /dev/null +++ b/internal/webhook/v2/weightsandbiases_proxy_test.go @@ -0,0 +1,54 @@ +package v2 + +import ( + "strings" + "testing" + + appsv2 "github.com/wandb/operator/api/v2" + corev1 "k8s.io/api/core/v1" +) + +func wandbWithProxy(proxy *appsv2.ProxySpec) *appsv2.WeightsAndBiases { + wandb := &appsv2.WeightsAndBiases{} + wandb.Spec.Global.Proxy = proxy + return wandb +} + +func TestValidateProxySpec(t *testing.T) { + secretRef := &appsv2.ProxyValueSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "egress-proxy"}, + Key: "httpsProxy", + }, + } + cases := []struct { + name string + proxy *appsv2.ProxySpec + wantErr string // substring; "" = accept + }{ + {"nil proxy", nil, ""}, + {"literal http url", &appsv2.ProxySpec{HTTPProxy: &appsv2.ProxyValue{Value: "http://proxy.corp:3128"}}, ""}, + {"secret-backed https", &appsv2.ProxySpec{HTTPSProxy: &appsv2.ProxyValue{ValueFrom: secretRef}}, ""}, + {"noProxy extras ok", &appsv2.ProxySpec{HTTPProxy: &appsv2.ProxyValue{Value: "http://p:3128"}, NoProxy: []string{"internal.example.com", "10.0.0.0/8"}}, ""}, + {"both value and valueFrom", &appsv2.ProxySpec{HTTPProxy: &appsv2.ProxyValue{Value: "http://p:3128", ValueFrom: secretRef}}, "exactly one"}, + {"neither value nor valueFrom", &appsv2.ProxySpec{HTTPProxy: &appsv2.ProxyValue{}}, "one of value or valueFrom is required"}, + {"userinfo in literal", &appsv2.ProxySpec{HTTPProxy: &appsv2.ProxyValue{Value: "http://user:pass@proxy:3128"}}, "must not contain credentials"}, + {"bad scheme", &appsv2.ProxySpec{HTTPProxy: &appsv2.ProxyValue{Value: "socks5://proxy:1080"}}, "scheme must be http or https"}, + {"comma in noProxy", &appsv2.ProxySpec{HTTPProxy: &appsv2.ProxyValue{Value: "http://p:3128"}, NoProxy: []string{"a,b"}}, "must not contain commas"}, + {"empty noProxy entry", &appsv2.ProxySpec{HTTPProxy: &appsv2.ProxyValue{Value: "http://p:3128"}, NoProxy: []string{""}}, "must not be empty"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + errs := validateProxySpec(wandbWithProxy(tc.proxy)) + if tc.wantErr == "" { + if len(errs) != 0 { + t.Fatalf("expected no errors, got %v", errs) + } + return + } + if len(errs) == 0 || !strings.Contains(errs.ToAggregate().Error(), tc.wantErr) { + t.Fatalf("expected error containing %q, got %v", tc.wantErr, errs) + } + }) + } +} diff --git a/internal/webhook/v2/weightsandbiases_webhook.go b/internal/webhook/v2/weightsandbiases_webhook.go index 0b1fd729..8c78fa31 100644 --- a/internal/webhook/v2/weightsandbiases_webhook.go +++ b/internal/webhook/v2/weightsandbiases_webhook.go @@ -19,6 +19,7 @@ package v2 import ( "context" "fmt" + "net/url" "strings" v1 "github.com/wandb/operator/api/v1" @@ -345,6 +346,7 @@ func validateSpec(_ context.Context, newWandb, oldWandb *appsv2.WeightsAndBiases networkingErrors, networkingWarnings := validateNetworkingSpec(newWandb) allErrors = append(allErrors, networkingErrors...) warnings = append(warnings, networkingWarnings...) + allErrors = append(allErrors, validateProxySpec(newWandb)...) if len(allErrors) == 0 { return warnings, nil @@ -781,6 +783,67 @@ func validateRedisChanges(newWandb, oldWandb *appsv2.WeightsAndBiases) field.Err return errors } +// validateProxySpec validates spec.global.proxy: each proxy value sets exactly +// one of value|valueFrom, a literal value parses as an http(s) URL with no +// userinfo (credentials must use valueFrom so they never land in the CR), and +// noProxy entries are non-empty and comma-free (the operator owns the join). +func validateProxySpec(wandb *appsv2.WeightsAndBiases) field.ErrorList { + var errors field.ErrorList + if wandb.Spec.Global.Proxy == nil { + return errors + } + proxy := wandb.Spec.Global.Proxy + base := field.NewPath("spec").Child("global").Child("proxy") + + validateValue := func(pv *appsv2.ProxyValue, child string) { + if pv == nil { + return + } + p := base.Child(child) + hasValue := pv.Value != "" + hasValueFrom := pv.ValueFrom != nil + switch { + case hasValue && hasValueFrom: + errors = append(errors, field.Invalid(p, pv, "set exactly one of value or valueFrom, not both")) + return + case !hasValue && !hasValueFrom: + errors = append(errors, field.Required(p, "one of value or valueFrom is required")) + return + } + if hasValueFrom && pv.ValueFrom.SecretKeyRef == nil { + errors = append(errors, field.Required(p.Child("valueFrom").Child("secretKeyRef"), + "valueFrom requires secretKeyRef")) + } + if hasValue { + parsed, err := url.Parse(pv.Value) + if err != nil { + errors = append(errors, field.Invalid(p.Child("value"), pv.Value, "must be a valid URL")) + return + } + if parsed.Scheme != "http" && parsed.Scheme != "https" { + errors = append(errors, field.Invalid(p.Child("value"), pv.Value, "scheme must be http or https")) + } + if parsed.User != nil { + errors = append(errors, field.Invalid(p.Child("value"), "[redacted]", + "must not contain credentials (userinfo); use valueFrom with a Secret for authenticated proxies")) + } + } + } + validateValue(proxy.HTTPProxy, "httpProxy") + validateValue(proxy.HTTPSProxy, "httpsProxy") + + for i, entry := range proxy.NoProxy { + p := base.Child("noProxy").Index(i) + if strings.TrimSpace(entry) == "" { + errors = append(errors, field.Invalid(p, entry, "must not be empty")) + } + if strings.Contains(entry, ",") { + errors = append(errors, field.Invalid(p, entry, "must not contain commas; use separate list entries")) + } + } + return errors +} + func validateNetworkingSpec(wandb *appsv2.WeightsAndBiases) (field.ErrorList, admission.Warnings) { var errors field.ErrorList var warnings admission.Warnings From 0d0bd62f1564e1fc7d88d809e43336232698518b Mon Sep 17 00:00:00 2001 From: wnevis-cmyk <wnevis@coreweave.com> Date: Wed, 22 Jul 2026 11:01:47 -0700 Subject: [PATCH 36/49] feat(backend): Add validator for hostname spec (#277) Co-authored-by: Cursor <cursoragent@cursor.com> --- .../weightsandbiases_multi_instance_test.go | 1 + .../webhook/v2/weightsandbiases_webhook.go | 14 ++++++++++ .../v2/weightsandbiases_webhook_test.go | 27 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/internal/webhook/v2/weightsandbiases_multi_instance_test.go b/internal/webhook/v2/weightsandbiases_multi_instance_test.go index 0934b6b2..5fc44694 100644 --- a/internal/webhook/v2/weightsandbiases_multi_instance_test.go +++ b/internal/webhook/v2/weightsandbiases_multi_instance_test.go @@ -72,6 +72,7 @@ var _ = Describe("WeightsAndBiases multi-instance infra", func() { wandb := &apiv2.WeightsAndBiases{ ObjectMeta: metav1.ObjectMeta{Name: "wb", Namespace: "ns"}, Spec: apiv2.WeightsAndBiasesSpec{ + Wandb: apiv2.WandbAppSpec{Hostname: "https://wandb.example.com"}, MySQL: map[string]apiv2.MySQLSpec{ apiv2.DefaultInstanceName: {ManagedMysql: &apiv2.ManagedMysqlSpec{Name: "wb-mysql", Namespace: "ns"}}, "analytics": {ManagedMysql: &apiv2.ManagedMysqlSpec{Name: "wb-mysql-analytics", Namespace: "ns"}}, diff --git a/internal/webhook/v2/weightsandbiases_webhook.go b/internal/webhook/v2/weightsandbiases_webhook.go index 8c78fa31..6e25a321 100644 --- a/internal/webhook/v2/weightsandbiases_webhook.go +++ b/internal/webhook/v2/weightsandbiases_webhook.go @@ -338,6 +338,7 @@ func validateSpec(_ context.Context, newWandb, oldWandb *appsv2.WeightsAndBiases var allErrors field.ErrorList var warnings admission.Warnings + allErrors = append(allErrors, validateWandbSpec(newWandb)...) allErrors = append(allErrors, validateMySQLSpec(newWandb)...) allErrors = append(allErrors, validateRedisSpec(newWandb)...) allErrors = append(allErrors, validateObjectStoreSpec(newWandb)...) @@ -425,6 +426,19 @@ func validateMySQLChanges(newWandb, oldWandb *appsv2.WeightsAndBiases) field.Err return errors } +func validateWandbSpec(wandb *appsv2.WeightsAndBiases) field.ErrorList { + var errors field.ErrorList + + if strings.TrimSpace(wandb.Spec.Wandb.Hostname) == "" { + errors = append(errors, field.Required( + field.NewPath("spec").Child("wandb").Child("hostname"), + "hostname is required", + )) + } + + return errors +} + func validateMySQLSpec(wandb *appsv2.WeightsAndBiases) field.ErrorList { var errors field.ErrorList mysqlPath := field.NewPath("spec").Child("mysql") diff --git a/internal/webhook/v2/weightsandbiases_webhook_test.go b/internal/webhook/v2/weightsandbiases_webhook_test.go index 93520967..96fde30a 100644 --- a/internal/webhook/v2/weightsandbiases_webhook_test.go +++ b/internal/webhook/v2/weightsandbiases_webhook_test.go @@ -39,7 +39,9 @@ var _ = Describe("WeightsAndBiases Webhook", func() { BeforeEach(func() { ctx = context.Background() obj = &appsv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "test-ns"}} + obj.Spec.Wandb.Hostname = "https://wandb.example.com" oldObj = &appsv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "test-ns"}} + oldObj.Spec.Wandb.Hostname = "https://wandb.example.com" validator = WeightsAndBiasesCustomValidator{} defaulter = WeightsAndBiasesCustomDefaulter{} }) @@ -124,6 +126,31 @@ var _ = Describe("WeightsAndBiases Webhook", func() { Expect(warnings).To(BeEmpty()) }) + It("rejects create when hostname is missing", func() { + obj.Spec.Wandb.Hostname = "" + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("spec.wandb.hostname")) + Expect(err.Error()).To(ContainSubstring("hostname is required")) + }) + + It("rejects create when hostname is only whitespace", func() { + obj.Spec.Wandb.Hostname = " " + + _, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("spec.wandb.hostname")) + }) + + It("rejects update when hostname is missing", func() { + obj.Spec.Wandb.Hostname = "" + + _, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("spec.wandb.hostname")) + }) + It("rejects create when Redis storage size is invalid", func() { obj.Spec.Redis = map[string]appsv2.RedisSpec{appsv2.DefaultInstanceName: {ManagedRedis: &appsv2.ManagedRedisSpec{StorageSize: "bad-size"}}} From dc615999d4902a4db4d340129eed9a5b94c765b4 Mon Sep 17 00:00:00 2001 From: Jagat Thakkar <32109558+jthakkar04@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:16:50 -0500 Subject: [PATCH 37/49] fix(telemetry): Point Kafka dashboard panels at Bufstream metrics (#286) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- .../wandb-managed-install-performance.json | 146 +++++++++++++++--- .../dashboards/wandb-telemetry-overview.json | 21 +-- 2 files changed, 139 insertions(+), 28 deletions(-) diff --git a/deploy/telemetry/dashboards/wandb-managed-install-performance.json b/deploy/telemetry/dashboards/wandb-managed-install-performance.json index 340ff97e..d5023356 100644 --- a/deploy/telemetry/dashboards/wandb-managed-install-performance.json +++ b/deploy/telemetry/dashboards/wandb-managed-install-performance.json @@ -376,50 +376,160 @@ }, { "type": "timeseries", - "title": "Kafka Messages In by Topic", + "title": "Kafka Request Rate by API", "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, "gridPos": {"h": 10, "w": 16, "x": 0, "y": 90}, "targets": [ { - "expr": "sum by (topic) (rate(kafka_server_brokertopicmetrics_messagesinpersec_total[$__rate_interval]))", - "legendFormat": "{{topic}}", + "expr": "sum by (kafka_api_key) (rate(bufstream_kafka_request_count_total{namespace=\"$namespace\"}[$__rate_interval]))", + "legendFormat": "{{kafka_api_key}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": {"unit": "reqps", "min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "max"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "stat", + "title": "Bufstream Status", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 4, "w": 8, "x": 16, "y": 90}, + "targets": [ + { + "expr": "max(bufstream_status{namespace=\"$namespace\"})", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "red", "value": 1}]} + }, + "overrides": [] + }, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, + "textMode": "value" + } + }, + { + "type": "timeseries", + "title": "Kafka Consumer Lag by Topic/Partition", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 100}, + "targets": [ + { + "expr": "max by (kafka_topic_name, kafka_topic_partition) (bufstream_kafka_consumer_group_offset_lag{namespace=\"$namespace\"})", + "legendFormat": "{{kafka_topic_name}} p{{kafka_topic_partition}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": {"unit": "short", "min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "max"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "timeseries", + "title": "Kafka Request Latency p95 by API", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 100}, + "targets": [ + { + "expr": "histogram_quantile(0.95, sum by (le, kafka_api_key) (rate(bufstream_kafka_request_latency_seconds_bucket{namespace=\"$namespace\"}[$__rate_interval])))", + "legendFormat": "{{kafka_api_key}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": {"unit": "s", "min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "max"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "timeseries", + "title": "Kafka Byte Throughput (in/out)", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 110}, + "targets": [ + { + "expr": "sum(rate(bufstream_kafka_request_bytes_sum{namespace=\"$namespace\"}[$__rate_interval]))", + "legendFormat": "in (requests)", "refId": "A" }, { - "expr": "sum(kafka_controller_kafkacontroller_offlinepartitionscount)", - "legendFormat": "offline partitions (total)", + "expr": "sum(rate(bufstream_kafka_response_bytes_sum{namespace=\"$namespace\"}[$__rate_interval]))", + "legendFormat": "out (responses)", "refId": "B" } ], "fieldConfig": { - "defaults": {"min": 0}, + "defaults": {"unit": "Bps", "min": 0}, "overrides": [] }, "options": { - "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "min", "max", "sum"]}, + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "max"]}, + "tooltip": {"mode": "multi"} + } + }, + { + "type": "timeseries", + "title": "Partitions by Topic", + "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 110}, + "targets": [ + { + "expr": "sum by (kafka_topic_name) (bufstream_kafka_topic_partition_count{namespace=\"$namespace\"})", + "legendFormat": "{{kafka_topic_name}}", + "refId": "A" + } + ], + "fieldConfig": { + "defaults": {"unit": "short", "min": 0}, + "overrides": [] + }, + "options": { + "legend": {"displayMode": "table", "placement": "bottom", "calcs": ["last"]}, "tooltip": {"mode": "multi"} } }, { "type": "text", "title": "About: Kafka", - "gridPos": {"h": 10, "w": 8, "x": 16, "y": 90}, + "gridPos": {"h": 6, "w": 8, "x": 16, "y": 94}, "options": { "mode": "markdown", - "content": "### What is this?\n\n- **Messages in by topic** — incoming message rate per Kafka topic, in messages/sec.\n- **offline partitions** — partitions that have no leader and can't accept reads or writes. Should always be 0.\n\n### Why it matters\n\nKafka is the asynchronous backbone for run-state events, parquet-export task scheduling, and other internal queues. When Kafka throughput drops or partitions go offline, downstream consumers stall and user-visible operations slow down.\n\n### What to do\n\n- **Offline partitions > 0**: investigate managed Kafka health immediately. `kubectl get pods -n <namespace> -l weightsandbiases.apps.wandb.com/component=kafka`, inspect logs, and check that Bufstream and etcd pods are Ready.\n- **Sudden drop in messages**: check whether the producer-side services (api, filestream) are healthy in the W&B Field Investigation dashboard." + "content": "### What is this?\n\nManaged Kafka is **Bufstream** — stateless and object-storage-backed, so broker-era metrics like offline / under-replicated partitions don't exist.\n\n- **Bufstream Status** — `max(bufstream_status)` across the health probes (etcd, object storage, metadata, kafka). **0 = healthy**; ≥1 means a probe is failing.\n- **Request rate / latency** — Kafka API throughput and p95 latency by request type (`bufstream_kafka_request_*`).\n- **Consumer lag** — committed-offset lag per topic/partition (`bufstream_kafka_consumer_group_offset_lag`); sustained growth means consumers are falling behind.\n- **Byte throughput** — bytes in/out on the wire.\n\n### What to do\n\n**Status ≥ 1 or lag climbing**: check managed Kafka health — `kubectl get pods -n <namespace> -l weightsandbiases.apps.wandb.com/component=kafka` and inspect the Bufstream and etcd pods." } }, { "type": "row", "title": "ClickHouse", - "gridPos": {"h": 1, "w": 24, "x": 0, "y": 100}, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 120}, "collapsed": false }, { "type": "timeseries", "title": "ClickHouse Memory, Connections, Merges", "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, - "gridPos": {"h": 10, "w": 16, "x": 0, "y": 101}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 121}, "targets": [ { "expr": "sum(ClickHouseMetrics_MemoryTracking)", @@ -451,7 +561,7 @@ { "type": "text", "title": "About: ClickHouse", - "gridPos": {"h": 10, "w": 8, "x": 16, "y": 101}, + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 121}, "options": { "mode": "markdown", "content": "### What is this?\n\n- **memory tracking** — total bytes ClickHouse is currently using across all queries.\n- **HTTP connections** — active HTTP client connections to ClickHouse.\n- **merges in progress** — background MergeTree merge operations currently running.\n\n### Why it matters\n\n- **Memory** climbing toward the configured limit signals query-side pressure. ClickHouse aborts queries when memory is exhausted, which surfaces as errors in dependent services.\n- **HTTP connections** persistently near zero may indicate clients can't reach ClickHouse.\n- **Merges** that climb without coming back down indicate ingest exceeding merge throughput; this leads to part-count limits and eventual write rejections.\n\n### What to do\n\nFor sustained memory pressure or merge backlog, increase the install size, which scales ClickHouse resources. For per-query memory diagnostics, query `system.query_log` directly on the ClickHouse pod." @@ -460,14 +570,14 @@ { "type": "row", "title": "Object Store", - "gridPos": {"h": 1, "w": 24, "x": 0, "y": 111}, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 131}, "collapsed": false }, { "type": "timeseries", "title": "Object Store Capacity Used % & Request Rate", "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, - "gridPos": {"h": 10, "w": 16, "x": 0, "y": 112}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 132}, "targets": [ { "expr": "100 * (1 - (sum(SeaweedFS_volumeServer_resource{type=\"free\"}) / sum(SeaweedFS_volumeServer_resource{type=\"all\"})))", @@ -503,7 +613,7 @@ { "type": "text", "title": "About: Object Store", - "gridPos": {"h": 10, "w": 8, "x": 16, "y": 112}, + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 132}, "options": { "mode": "markdown", "content": "### What is this?\n\n- **capacity used %** — percentage of usable volume-server capacity in use, derived from `SeaweedFS_volumeServer_resource{type=\"free\"|\"all\"}`.\n- **requests/sec** — filer HTTP request rate across all operations (`SeaweedFS_filer_request_total`).\n\n### Why it matters\n\nSeaweedFS is the object store for parquet files, artifact contents, and run media.\n\n- **Capacity above 90%** = urgent action required. New writes will fail soon.\n- **Capacity above 75%** = plan a storage increase.\n- **Sudden drop in request rate** = the API or executor services may be unable to reach the filer; check the seaweedfs Service and pod logs.\n\n### What to do\n\nFor capacity: increase storage in the WeightsAndBiases CR (`spec.objectStore.managedObjectStore.storageSize`) and re-apply, or migrate older artifacts off-cluster. For request failures: `kubectl logs -n <namespace> -l app.kubernetes.io/managed-by=seaweedfs-operator,app.kubernetes.io/component=filer --tail=200`." @@ -512,14 +622,14 @@ { "type": "row", "title": "Storage Path Diagnostics", - "gridPos": {"h": 1, "w": 24, "x": 0, "y": 122}, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 142}, "collapsed": false }, { "type": "timeseries", "title": "Slowest Storage Operations (p95, by span)", "datasource": {"type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}"}, - "gridPos": {"h": 10, "w": 16, "x": 0, "y": 123}, + "gridPos": {"h": 10, "w": 16, "x": 0, "y": 143}, "targets": [ { "expr": "topk(10, histogram_quantile(0.95, sum by (service_name, span_name, le) (rate(traces_spanmetrics_duration_milliseconds_bucket{service_name=~\"gorilla.*\", span_name=~\"(?i)(parquet|filestream|filehandler|runstore|historystore|metadatastore|s3|sql).*\"}[$__rate_interval]))))", @@ -539,7 +649,7 @@ { "type": "text", "title": "About: Slow Storage Operations", - "gridPos": {"h": 10, "w": 8, "x": 16, "y": 123}, + "gridPos": {"h": 10, "w": 8, "x": 16, "y": 143}, "options": { "mode": "markdown", "content": "### What is this?\n\np95 latency per storage operation, derived from traces by the OTel `spanmetrics` connector. Covers parquet reads/writes, filestream chunks, S3 calls, and SQL statements.\n\n### Why it matters\n\nSlow chart loads, slow file uploads, and stuck artifact downloads almost always trace back to one storage path being slow. This panel surfaces which one without opening individual traces.\n\n### What to do\n\n- Spikes on `ParquetHistoryStore.*` or `HistoryStore.*` → chart loads will feel slow. Check the parquet pod's CPU/memory in **Container Resource Usage** above, and ClickHouse load if installed.\n- Spikes on `FileStreamStore.*` / `FileHandler.*` → ingest stalls. Check filestream container restarts and object store capacity below.\n- Spikes on `sql.*` → MySQL is the bottleneck. See **MySQL Errors & Slow Queries** above.\n- For per-request detail, [open Traces in Explore](/explore?panes=%7B%22A%22:%7B%22datasource%22:%22${DS_VICTORIATRACES}%22,%22queries%22:[],%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D%7D&schemaVersion=1&orgId=1) and filter by the offending `service.name` + `span.name` with `duration > 1s`." diff --git a/deploy/telemetry/dashboards/wandb-telemetry-overview.json b/deploy/telemetry/dashboards/wandb-telemetry-overview.json index 0367215b..b641567c 100644 --- a/deploy/telemetry/dashboards/wandb-telemetry-overview.json +++ b/deploy/telemetry/dashboards/wandb-telemetry-overview.json @@ -1861,7 +1861,7 @@ }, { "type": "stat", - "title": "Kafka Messages / sec", + "title": "Kafka Requests / sec", "datasource": { "type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}" @@ -1874,7 +1874,7 @@ }, "targets": [ { - "expr": "sum(rate(kafka_server_brokertopicmetrics_messagesinpersec_total[$__rate_interval]))", + "expr": "sum(rate(bufstream_kafka_request_count_total{namespace=\"$namespace\"}[$__rate_interval]))", "refId": "A" } ], @@ -1895,7 +1895,7 @@ }, { "type": "stat", - "title": "Offline Partitions", + "title": "Bufstream Status", "datasource": { "type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}" @@ -1908,7 +1908,7 @@ }, "targets": [ { - "expr": "sum(kafka_controller_kafkacontroller_offlinepartitionscount)", + "expr": "max(bufstream_status{namespace=\"$namespace\"})", "refId": "A" } ], @@ -2046,7 +2046,7 @@ }, { "type": "timeseries", - "title": "Kafka Messages by Topic", + "title": "Kafka Consumer Lag by Topic", "datasource": { "type": "victoriametrics-metrics-datasource", "uid": "${DS_VICTORIAMETRICS}" @@ -2059,11 +2059,12 @@ }, "targets": [ { - "expr": "sum(rate(kafka_server_brokertopicmetrics_messagesinpersec_total[$__rate_interval])) by (topic)", - "legendFormat": "{{topic}}", + "expr": "max by (kafka_topic_name) (bufstream_kafka_consumer_group_offset_lag{namespace=\"$namespace\"})", + "legendFormat": "{{kafka_topic_name}}", "refId": "A" } - ] + ], + "fieldConfig": {"defaults": {"unit": "short", "min": 0}, "overrides": []} }, { "type": "timeseries", @@ -2192,7 +2193,7 @@ "gridPos": {"h": 8, "w": 8, "x": 16, "y": 146}, "options": { "mode": "markdown", - "content": "**What it shows** — whether each ingestion path on the gateway is receiving data. This is how you confirm **DataDog** traces/metrics are populated.\n\n- **`datadog`** — DataDog-APM traces (port 8126). A non-zero line here means DataDog-protocol tracing is flowing; downstream it appears as `traces_spanmetrics_calls_total` for the SDK services.\n- **`otlp`** — the main gorilla OpenTelemetry path (the bulk of spans/metrics).\n- **`statsd`** — DogStatsD metrics (port 8125). **Expect this to be absent/zero** unless gorilla runs with `GORILLA_STATSD_PORT` > 0 (a core/manifest-side setting). Zero here is the reason the DogStatsD-only panels were dropped.\n\n**What bad looks like** — a receiver you expect traffic on flatlining at 0, or `otelcol_receiver_refused_*` climbing (data arriving but rejected)." + "content": "**What it shows** — whether each ingestion path on the gateway is receiving data. This is how you confirm **DataDog** traces/metrics are populated.\n\n- **`datadog`** — DataDog-APM traces (port 8126). A non-zero line here means DataDog-protocol tracing is flowing; downstream it appears as `traces_spanmetrics_calls_milliseconds_total` for the SDK services.\n- **`otlp`** — the main gorilla OpenTelemetry path (the bulk of spans/metrics).\n- **`statsd`** — DogStatsD metrics (port 8125). Gorilla emits these via `GORILLA_STATSD_ADDRESS` regardless of the `GORILLA_STATSD_PORT` setting (`GORILLA_STATSD_PORT=0` is a vestigial no-op), so datagrams do reach the collector. This line can look thin on an idle cluster because gorilla emits most app metrics only under traffic — not because DogStatsD is disabled.\n\n**What bad looks like** — a receiver you expect traffic on flatlining at 0, or `otelcol_receiver_refused_*` climbing (data arriving but rejected)." } }, { @@ -2209,7 +2210,7 @@ ], "fieldConfig": {"defaults": {"unit": "cps", "min": 0}, "overrides": []}, "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "max"]}, "tooltip": {"mode": "multi"}}, - "description": "Metric points/sec the collector accepts, split by receiver. A `statsd` line appears only when gorilla emits DogStatsD (GORILLA_STATSD_PORT > 0); today only `otlp` carries metrics." + "description": "Metric points/sec the collector accepts, split by receiver. Gorilla emits DogStatsD via `GORILLA_STATSD_ADDRESS`, so a `statsd` line can appear here; it may be sparse on an idle cluster because most app metrics are emitted only under traffic." }, { "type": "text", From e19c7aeb54cb233c1f3a53b89d3355c4f03e624d Mon Sep 17 00:00:00 2001 From: Casey Abernathy <cabernathy@coreweave.com> Date: Thu, 23 Jul 2026 15:44:01 -0700 Subject: [PATCH 38/49] fix: Resolve lint and image scan failures (#297) --- go.mod | 8 ++++---- go.sum | 12 ++++++------ internal/controller/ctrlqueue/ctrl_state_test.go | 1 - pkg/wandb/spec/charts/oci.go | 8 ++++++-- pkg/wandb/spec/charts/repo.go | 4 +++- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 95cd84a7..22fb427a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/wandb/operator -go 1.26.3 +go 1.26.5 godebug default=go1.26 @@ -181,9 +181,9 @@ require ( golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.45.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect - google.golang.org/grpc v1.80.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index cd5062de..615f09c7 100644 --- a/go.sum +++ b/go.sum @@ -469,12 +469,12 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI= -google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/controller/ctrlqueue/ctrl_state_test.go b/internal/controller/ctrlqueue/ctrl_state_test.go index 20395c7c..4b636c43 100644 --- a/internal/controller/ctrlqueue/ctrl_state_test.go +++ b/internal/controller/ctrlqueue/ctrl_state_test.go @@ -201,7 +201,6 @@ var _ = Describe("CtrlState", func() { result, err := state.ReconcilerResult() Expect(err).To(BeNil()) Expect(result).To(Equal(ctrl.Result{})) - Expect(result.Requeue).To(BeFalse()) // nolint:SA1019 Expect(result.RequeueAfter).To(Equal(time.Duration(0))) }) diff --git a/pkg/wandb/spec/charts/oci.go b/pkg/wandb/spec/charts/oci.go index 8854b40c..ab7c2fc8 100644 --- a/pkg/wandb/spec/charts/oci.go +++ b/pkg/wandb/spec/charts/oci.go @@ -61,8 +61,12 @@ func validateOCIVersion(fl validator.FieldLevel) bool { func (c OCIRelease) Validate() error { v := validator.New() - v.RegisterValidation("ociurl", validateOCIURL) - v.RegisterValidation("ociversion", validateOCIVersion) + if err := v.RegisterValidation("ociurl", validateOCIURL); err != nil { + return fmt.Errorf("register OCI URL validation: %w", err) + } + if err := v.RegisterValidation("ociversion", validateOCIVersion); err != nil { + return fmt.Errorf("register OCI version validation: %w", err) + } return v.Struct(c) } diff --git a/pkg/wandb/spec/charts/repo.go b/pkg/wandb/spec/charts/repo.go index 85814eed..2229b247 100644 --- a/pkg/wandb/spec/charts/repo.go +++ b/pkg/wandb/spec/charts/repo.go @@ -84,7 +84,9 @@ func validateNonOCIURL(fl validator.FieldLevel) bool { func (c RepoRelease) Validate() error { v := validator.New() - v.RegisterValidation("nonociurl", validateNonOCIURL) + if err := v.RegisterValidation("nonociurl", validateNonOCIURL); err != nil { + return fmt.Errorf("register non-OCI URL validation: %w", err) + } return v.Struct(c) } From 6f68c8bbb7eb329d163871cc2218f1e77156aba4 Mon Sep 17 00:00:00 2001 From: Casey Abernathy <cabernathy@coreweave.com> Date: Mon, 27 Jul 2026 10:01:07 -0700 Subject: [PATCH 39/49] fix: Support workload identity for managed storage clients (#293) --- api/v2/weightsandbiases_types.go | 30 ++++++++--- api/v2/zz_generated.deepcopy.go | 29 ++++++++++ .../apps.wandb.com_weightsandbiases.yaml | 22 ++++++++ .../infra/managed/clickhouse/altinity/spec.go | 44 +++++++++++++-- .../managed/clickhouse/altinity/spec_test.go | 48 +++++++++++++++++ .../managed/clickhouse/altinity/write.go | 36 +++++++++++++ .../infra/managed/kafka/bufstream/spec.go | 30 ++++++++--- .../managed/kafka/bufstream/spec_test.go | 54 ++++++++++++++++++- .../infra/managed/kafka/bufstream/write.go | 10 +++- internal/controller/reconciler/clickhouse.go | 14 ++++- .../apps.wandb.com_weightsandbiases.yaml | 22 ++++++++ ...ghtsandbiases_defaulter_clickhouse_test.go | 3 ++ .../weightsandbiases_defaulter_kafka_test.go | 3 ++ .../webhook/v2/weightsandbiases_webhook.go | 12 +++++ 14 files changed, 334 insertions(+), 23 deletions(-) diff --git a/api/v2/weightsandbiases_types.go b/api/v2/weightsandbiases_types.go index b561a1d5..ca83f6a8 100644 --- a/api/v2/weightsandbiases_types.go +++ b/api/v2/weightsandbiases_types.go @@ -428,6 +428,18 @@ type ServiceAccountSpec struct { Annotations map[string]string `json:"annotations,omitempty"` } +// ManagedServiceAccountSpec configures the Kubernetes identity used by a +// managed infrastructure workload. +type ManagedServiceAccountSpec struct { + // Create controls whether the operator reconciles the ServiceAccount. It + // defaults to true; set it to false to reference an existing identity. + Create *bool `json:"create,omitempty"` + // ServiceAccountName defaults to the managed infrastructure resource name. + ServiceAccountName string `json:"serviceAccountName,omitempty"` + // Annotations supports cloud workload identity integrations such as IRSA. + Annotations map[string]string `json:"annotations,omitempty"` +} + type InternalServiceAuth struct { Enabled *bool `json:"enabled,omitempty"` OIDCIssuer string `json:"oidcIssuer,omitempty"` @@ -546,13 +558,15 @@ type KafkaSpec struct { type ManagedKafkaSpec struct { ManagedInfraSpec `json:",inline"` - StorageSize string `json:"storageSize,omitempty"` - Replicas int32 `json:"replicas,omitempty"` - Config KafkaConfig `json:"config,omitempty"` - Namespace string `json:"namespace,omitempty"` - Name string `json:"name,omitempty"` - Telemetry Telemetry `json:"telemetry,omitempty"` - SkipDataRecovery bool `json:"skipDataRecovery,omitempty"` + StorageSize string `json:"storageSize,omitempty"` + Replicas int32 `json:"replicas,omitempty"` + Config KafkaConfig `json:"config,omitempty"` + Namespace string `json:"namespace,omitempty"` + Name string `json:"name,omitempty"` + Telemetry Telemetry `json:"telemetry,omitempty"` + // ServiceAccount configures the identity used by the Bufstream broker. + ServiceAccount ManagedServiceAccountSpec `json:"serviceAccount,omitempty"` + SkipDataRecovery bool `json:"skipDataRecovery,omitempty"` } type KafkaConnection struct { @@ -655,6 +669,8 @@ type ManagedClickHouseSpec struct { Namespace string `json:"namespace,omitempty"` Name string `json:"name,omitempty"` Telemetry Telemetry `json:"telemetry,omitempty"` + // ServiceAccount configures the identity used by ClickHouse server pods. + ServiceAccount ManagedServiceAccountSpec `json:"serviceAccount,omitempty"` // ObjectStorage configures the S3-backed disk that holds ClickHouse table // data in the configured W&B object store (managed SeaweedFS or external diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index 2ee62bf9..e1ebfcdd 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -767,6 +767,7 @@ func (in *ManagedClickHouseSpec) DeepCopyInto(out *ManagedClickHouseSpec) { in.ManagedInfraSpec.DeepCopyInto(&out.ManagedInfraSpec) in.Config.DeepCopyInto(&out.Config) out.Telemetry = in.Telemetry + in.ServiceAccount.DeepCopyInto(&out.ServiceAccount) out.ObjectStorage = in.ObjectStorage in.Keeper.DeepCopyInto(&out.Keeper) } @@ -823,6 +824,7 @@ func (in *ManagedKafkaSpec) DeepCopyInto(out *ManagedKafkaSpec) { in.ManagedInfraSpec.DeepCopyInto(&out.ManagedInfraSpec) in.Config.DeepCopyInto(&out.Config) out.Telemetry = in.Telemetry + in.ServiceAccount.DeepCopyInto(&out.ServiceAccount) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedKafkaSpec. @@ -891,6 +893,33 @@ func (in *ManagedRedisSpec) DeepCopy() *ManagedRedisSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedServiceAccountSpec) DeepCopyInto(out *ManagedServiceAccountSpec) { + *out = *in + if in.Create != nil { + in, out := &in.Create, &out.Create + *out = new(bool) + **out = **in + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedServiceAccountSpec. +func (in *ManagedServiceAccountSpec) DeepCopy() *ManagedServiceAccountSpec { + if in == nil { + return nil + } + out := new(ManagedServiceAccountSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MigrationJobStatus) DeepCopyInto(out *MigrationJobStatus) { *out = *in diff --git a/config/crd/bases/apps.wandb.com_weightsandbiases.yaml b/config/crd/bases/apps.wandb.com_weightsandbiases.yaml index 09deb2e0..ff472e5c 100644 --- a/config/crd/bases/apps.wandb.com_weightsandbiases.yaml +++ b/config/crd/bases/apps.wandb.com_weightsandbiases.yaml @@ -1157,6 +1157,17 @@ spec: required: - onDelete type: object + serviceAccount: + properties: + annotations: + additionalProperties: + type: string + type: object + create: + type: boolean + serviceAccountName: + type: string + type: object storageSize: type: string telemetry: @@ -1760,6 +1771,17 @@ spec: required: - onDelete type: object + serviceAccount: + properties: + annotations: + additionalProperties: + type: string + type: object + create: + type: boolean + serviceAccountName: + type: string + type: object skipDataRecovery: type: boolean storageSize: diff --git a/internal/controller/infra/managed/clickhouse/altinity/spec.go b/internal/controller/infra/managed/clickhouse/altinity/spec.go index cd8dcca4..2aa9bf6e 100644 --- a/internal/controller/infra/managed/clickhouse/altinity/spec.go +++ b/internal/controller/infra/managed/clickhouse/altinity/spec.go @@ -118,6 +118,40 @@ func writableEmptyDirVolume(name string) corev1.Volume { } } +func ToServiceAccount( + wandb *apiv2.WeightsAndBiases, + spec *apiv2.ManagedClickHouseSpec, + objStorage *ObjectStorageConn, + scheme *runtime.Scheme, +) (*corev1.ServiceAccount, error) { + if spec.ServiceAccount.Create != nil && !*spec.ServiceAccount.Create { + return nil, nil + } + + serviceAccount := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: clickHouseServiceAccountName(spec), + Namespace: spec.Namespace, + Labels: BuildWandbClickhouseLabels(wandb), + Annotations: spec.ServiceAccount.Annotations, + }, + AutomountServiceAccountToken: ptr.To(objStorage.UseEnvCredentials), + } + if wandb.Namespace == spec.Namespace { + if err := ctrl.SetControllerReference(wandb, serviceAccount, scheme); err != nil { + return nil, fmt.Errorf("failed to set owner reference on ClickHouse ServiceAccount: %w", err) + } + } + return serviceAccount, nil +} + +func clickHouseServiceAccountName(spec *apiv2.ManagedClickHouseSpec) string { + if spec.ServiceAccount.ServiceAccountName != "" { + return spec.ServiceAccount.ServiceAccountName + } + return spec.Name +} + // ToClickHouseVendorSpec converts a ClickHouseSpec to a ClickHouseInstallation CR. // This function translates the high-level ClickHouse spec into the vendor-specific // ClickHouseInstallation format used by the Altinity operator. @@ -191,10 +225,12 @@ func ToClickHouseVendorSpec( clickHouseImage := ClickHouseImage(mfst.Clickhouse["default"].Images["server"], wandb.Spec.Global.ImageRegistry) podSpec := corev1.PodSpec{ - SecurityContext: clickHousePodSecurityContext(), - Affinity: wandb.GetAffinity(spec.ManagedInfraSpec), - Tolerations: *wandb.GetTolerations(spec.ManagedInfraSpec), - Volumes: clickHouseWritableVolumes(), + ServiceAccountName: clickHouseServiceAccountName(spec), + AutomountServiceAccountToken: ptr.To(objStorage.UseEnvCredentials), + SecurityContext: clickHousePodSecurityContext(), + Affinity: wandb.GetAffinity(spec.ManagedInfraSpec), + Tolerations: *wandb.GetTolerations(spec.ManagedInfraSpec), + Volumes: clickHouseWritableVolumes(), Containers: []corev1.Container{ { Name: "clickhouse", diff --git a/internal/controller/infra/managed/clickhouse/altinity/spec_test.go b/internal/controller/infra/managed/clickhouse/altinity/spec_test.go index 5788ce4f..fe219f70 100644 --- a/internal/controller/infra/managed/clickhouse/altinity/spec_test.go +++ b/internal/controller/infra/managed/clickhouse/altinity/spec_test.go @@ -30,6 +30,9 @@ var _ = Describe("ClickHouse vendor specs", func() { Expect(chi.Spec.Templates.PodTemplates).To(HaveLen(1)) podSpec := chi.Spec.Templates.PodTemplates[0].Spec + Expect(podSpec.ServiceAccountName).To(Equal("clickhouse")) + Expect(podSpec.AutomountServiceAccountToken).NotTo(BeNil()) + Expect(*podSpec.AutomountServiceAccountToken).To(BeFalse()) expectClickHouseDefaultPodSecurityContext(podSpec.SecurityContext) expectClickHouseWritableVolume(podSpec.Volumes, clickHouseTmpVolumeName) expectClickHouseWritableVolume(podSpec.Volumes, clickHouseLogVolumeName) @@ -111,6 +114,51 @@ var _ = Describe("ClickHouse vendor specs", func() { Expect(err).NotTo(HaveOccurred()) Expect(chi.Spec.Templates.PodTemplates[0].Spec.InitContainers).To(BeEmpty()) }) + + It("wires ambient object storage credentials through the configured service account", func() { + wandb := clickHouseWandb() + spec := wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse + spec.ServiceAccount = apiv2.ManagedServiceAccountSpec{ + ServiceAccountName: "clickhouse-workload-identity", + Annotations: map[string]string{ + "eks.amazonaws.com/role-arn": "arn:aws:iam::123456789012:role/wandb-clickhouse", + }, + } + objStorage := testObjectStorageConn() + objStorage.UseEnvCredentials = true + + serviceAccount, err := ToServiceAccount(wandb, spec, objStorage, clickHouseScheme()) + Expect(err).NotTo(HaveOccurred()) + Expect(serviceAccount.Name).To(Equal("clickhouse-workload-identity")) + Expect(serviceAccount.Annotations).To(Equal(spec.ServiceAccount.Annotations)) + Expect(serviceAccount.AutomountServiceAccountToken).NotTo(BeNil()) + Expect(*serviceAccount.AutomountServiceAccountToken).To(BeTrue()) + + chi, err := ToClickHouseVendorSpec(context.Background(), wandb, spec, clickHouseScheme(), objStorage, false, manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + podSpec := chi.Spec.Templates.PodTemplates[0].Spec + Expect(podSpec.ServiceAccountName).To(Equal(serviceAccount.Name)) + Expect(podSpec.AutomountServiceAccountToken).NotTo(BeNil()) + Expect(*podSpec.AutomountServiceAccountToken).To(BeTrue()) + }) + + It("can reference an existing ClickHouse service account", func() { + wandb := clickHouseWandb() + spec := wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse + create := false + spec.ServiceAccount = apiv2.ManagedServiceAccountSpec{ + Create: &create, + ServiceAccountName: "existing-clickhouse-identity", + } + + serviceAccount, err := ToServiceAccount(wandb, spec, testObjectStorageConn(), clickHouseScheme()) + Expect(err).NotTo(HaveOccurred()) + Expect(serviceAccount).To(BeNil()) + + chi, err := ToClickHouseVendorSpec(context.Background(), wandb, spec, clickHouseScheme(), testObjectStorageConn(), false, manifest.Manifest{}) + Expect(err).NotTo(HaveOccurred()) + Expect(chi.Spec.Templates.PodTemplates[0].Spec.ServiceAccountName).To(Equal("existing-clickhouse-identity")) + }) }) func testObjectStorageConn() *ObjectStorageConn { diff --git a/internal/controller/infra/managed/clickhouse/altinity/write.go b/internal/controller/infra/managed/clickhouse/altinity/write.go index 1d0ceb65..e5456bba 100644 --- a/internal/controller/infra/managed/clickhouse/altinity/write.go +++ b/internal/controller/infra/managed/clickhouse/altinity/write.go @@ -8,6 +8,7 @@ import ( "github.com/wandb/operator/internal/logx" chkv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1" chiv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -25,12 +26,20 @@ func WriteState( ctx context.Context, client client.Client, specNamespacedName types.NamespacedName, + desiredServiceAccount *corev1.ServiceAccount, desiredKeeper *chkv1.ClickHouseKeeperInstallation, desired *chiv1.ClickHouseInstallation, ) []metav1.Condition { ctx, _ = logx.WithSlog(ctx, logx.ClickHouse) results := make([]metav1.Condition, 0) + if desiredServiceAccount != nil { + serviceAccountConditions := writeServiceAccount(ctx, client, desiredServiceAccount) + results = append(results, serviceAccountConditions...) + if len(serviceAccountConditions) > 0 { + return results + } + } results = append(results, keeper.WriteState( ctx, client, types.NamespacedName{Namespace: desiredKeeper.Namespace, Name: desiredKeeper.Name}, @@ -41,6 +50,33 @@ func WriteState( return results } +func writeServiceAccount( + ctx context.Context, + cl client.Client, + desired *corev1.ServiceAccount, +) []metav1.Condition { + actual := &corev1.ServiceAccount{} + found, err := common.GetResource(ctx, cl, client.ObjectKeyFromObject(desired), "ServiceAccount", actual) + if err != nil { + return []metav1.Condition{{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }} + } + if !found { + actual = nil + } + if _, err := common.CrudResource(ctx, cl, desired, actual); err != nil { + return []metav1.Condition{{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + }} + } + return nil +} + // writeClickHouseInstallation create-or-updates the CHI, setting only the fields // we own (spec, labels, owner refs) and preserving the Altinity-managed // finalizer/status. It compares owned fields via JSON, never the vendored status diff --git a/internal/controller/infra/managed/kafka/bufstream/spec.go b/internal/controller/infra/managed/kafka/bufstream/spec.go index aee773de..c8ead740 100644 --- a/internal/controller/infra/managed/kafka/bufstream/spec.go +++ b/internal/controller/infra/managed/kafka/bufstream/spec.go @@ -212,15 +212,22 @@ func ToConfigMap( func ToServiceAccount( wandb *apiv2.WeightsAndBiases, nsnBuilder *NsNameBuilder, + storage objectstore.ConnInfo, scheme *runtime.Scheme, ) (*corev1.ServiceAccount, error) { + spec := wandb.Spec.Kafka.ManagedKafka + if spec.ServiceAccount.Create != nil && !*spec.ServiceAccount.Create { + return nil, nil + } + sa := &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ - Name: nsnBuilder.ServiceAccountName(), - Namespace: nsnBuilder.Namespace(), - Labels: BuildWandbKafkaLabels(wandb), + Name: kafkaServiceAccountName(spec), + Namespace: nsnBuilder.Namespace(), + Labels: BuildWandbKafkaLabels(wandb), + Annotations: spec.ServiceAccount.Annotations, }, - AutomountServiceAccountToken: ptr.To(false), + AutomountServiceAccountToken: ptr.To(!storage.HasStaticCredentials()), } if err := setOwner(wandb, sa, nsnBuilder, scheme); err != nil { return nil, err @@ -248,7 +255,7 @@ func ToSccRoleBinding( Subjects: []rbacv1.Subject{ { Kind: "ServiceAccount", - Name: nsnBuilder.ServiceAccountName(), + Name: kafkaServiceAccountName(wandb.Spec.Kafka.ManagedKafka), Namespace: nsnBuilder.Namespace(), }, }, @@ -315,7 +322,7 @@ func ToEtcdApplication( }, PodTemplate: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ - ServiceAccountName: nsnBuilder.ServiceAccountName(), + ServiceAccountName: kafkaServiceAccountName(infraSpec), AutomountServiceAccountToken: ptr.To(false), SecurityContext: kafkaPodSecurityContext(), Affinity: spreadAffinity(wandb, infraSpec.ManagedInfraSpec, labels), @@ -567,8 +574,8 @@ func ToBufstreamApplication( }, PodTemplate: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ - ServiceAccountName: nsnBuilder.ServiceAccountName(), - AutomountServiceAccountToken: ptr.To(false), + ServiceAccountName: kafkaServiceAccountName(infraSpec), + AutomountServiceAccountToken: ptr.To(!storage.HasStaticCredentials()), SecurityContext: bufstreamPodSecurityContext(), Affinity: spreadAffinity(wandb, infraSpec.ManagedInfraSpec, labels), Tolerations: tolerations(wandb, infraSpec.ManagedInfraSpec), @@ -602,3 +609,10 @@ func ToBufstreamApplication( } return app, nil } + +func kafkaServiceAccountName(spec *apiv2.ManagedKafkaSpec) string { + if spec.ServiceAccount.ServiceAccountName != "" { + return spec.ServiceAccount.ServiceAccountName + } + return spec.Name +} diff --git a/internal/controller/infra/managed/kafka/bufstream/spec_test.go b/internal/controller/infra/managed/kafka/bufstream/spec_test.go index 344d4ea4..c0684ada 100644 --- a/internal/controller/infra/managed/kafka/bufstream/spec_test.go +++ b/internal/controller/infra/managed/kafka/bufstream/spec_test.go @@ -226,11 +226,45 @@ func TestApplicationsUseDedicatedServiceAccount(t *testing.T) { require.False(t, *bufstream.Spec.PodTemplate.Spec.AutomountServiceAccountToken) } +func TestApplicationsUseWorkloadIdentityForAmbientCredentials(t *testing.T) { + setOpenShiftMode(t, false) + wandb := testWandb() + wandb.Spec.Kafka.ManagedKafka.ServiceAccount = apiv2.ManagedServiceAccountSpec{ + ServiceAccountName: "kafka-workload-identity", + Annotations: map[string]string{ + "eks.amazonaws.com/role-arn": "arn:aws:iam::123456789012:role/wandb-kafka", + }, + } + nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) + storage := testStorage() + storage.AccessKey = "" + storage.SecretKey = "" + + sa, err := ToServiceAccount(wandb, nsn, storage, testScheme(t)) + require.NoError(t, err) + require.Equal(t, "kafka-workload-identity", sa.Name) + require.Equal(t, wandb.Spec.Kafka.ManagedKafka.ServiceAccount.Annotations, sa.Annotations) + require.NotNil(t, sa.AutomountServiceAccountToken) + require.True(t, *sa.AutomountServiceAccountToken) + + bufstream, err := ToBufstreamApplication(wandb, nsn, storage, false, testScheme(t), manifest.Manifest{}) + require.NoError(t, err) + require.Equal(t, sa.Name, bufstream.Spec.PodTemplate.Spec.ServiceAccountName) + require.NotNil(t, bufstream.Spec.PodTemplate.Spec.AutomountServiceAccountToken) + require.True(t, *bufstream.Spec.PodTemplate.Spec.AutomountServiceAccountToken) + + etcd, err := ToEtcdApplication(wandb, nsn, testScheme(t), manifest.Manifest{}) + require.NoError(t, err) + require.Equal(t, sa.Name, etcd.Spec.PodTemplate.Spec.ServiceAccountName) + require.NotNil(t, etcd.Spec.PodTemplate.Spec.AutomountServiceAccountToken) + require.False(t, *etcd.Spec.PodTemplate.Spec.AutomountServiceAccountToken) +} + func TestToServiceAccount(t *testing.T) { wandb := testWandb() nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) - sa, err := ToServiceAccount(wandb, nsn, testScheme(t)) + sa, err := ToServiceAccount(wandb, nsn, testStorage(), testScheme(t)) require.NoError(t, err) require.Equal(t, nsn.ServiceAccountName(), sa.Name) require.Equal(t, "default", sa.Namespace) @@ -240,6 +274,24 @@ func TestToServiceAccount(t *testing.T) { require.Len(t, sa.OwnerReferences, 1) } +func TestToServiceAccountCanReferenceExistingIdentity(t *testing.T) { + wandb := testWandb() + create := false + wandb.Spec.Kafka.ManagedKafka.ServiceAccount = apiv2.ManagedServiceAccountSpec{ + Create: &create, + ServiceAccountName: "existing-kafka-identity", + } + nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) + + sa, err := ToServiceAccount(wandb, nsn, testStorage(), testScheme(t)) + require.NoError(t, err) + require.Nil(t, sa) + + app, err := ToBufstreamApplication(wandb, nsn, testStorage(), false, testScheme(t), manifest.Manifest{}) + require.NoError(t, err) + require.Equal(t, "existing-kafka-identity", app.Spec.PodTemplate.Spec.ServiceAccountName) +} + func TestToSccRoleBinding(t *testing.T) { wandb := testWandb() nsn := CreateNsNameBuilder(types.NamespacedName{Namespace: "default", Name: "wandb-kafka"}) diff --git a/internal/controller/infra/managed/kafka/bufstream/write.go b/internal/controller/infra/managed/kafka/bufstream/write.go index 6cdad35a..d1e8cc81 100644 --- a/internal/controller/infra/managed/kafka/bufstream/write.go +++ b/internal/controller/infra/managed/kafka/bufstream/write.go @@ -56,7 +56,7 @@ func WriteState( if err != nil { return translateError(err) } - serviceAccount, err := ToServiceAccount(wandb, nsnBuilder, cl.Scheme()) + serviceAccount, err := ToServiceAccount(wandb, nsnBuilder, storage, cl.Scheme()) if err != nil { return translateError(err) } @@ -82,7 +82,13 @@ func WriteState( } results = append(results, writeResource(ctx, cl, common.ReconciledType, SecretResourceType, credsSecret, &corev1.Secret{})...) results = append(results, writeResource(ctx, cl, common.ReconciledType, ConfigMapResourceType, configMap, &corev1.ConfigMap{})...) - results = append(results, writeResource(ctx, cl, common.ReconciledType, ServiceAccountResourceType, serviceAccount, &corev1.ServiceAccount{})...) + if serviceAccount != nil { + serviceAccountConditions := writeResource(ctx, cl, common.ReconciledType, ServiceAccountResourceType, serviceAccount, &corev1.ServiceAccount{}) + results = append(results, serviceAccountConditions...) + if len(serviceAccountConditions) > 0 { + return results + } + } if sccRoleBinding != nil { results = append(results, writeResource(ctx, cl, common.ReconciledType, RoleBindingResourceType, sccRoleBinding, &rbacv1.RoleBinding{})...) } diff --git a/internal/controller/reconciler/clickhouse.go b/internal/controller/reconciler/clickhouse.go index bfdff2ea..ca6dc153 100644 --- a/internal/controller/reconciler/clickhouse.go +++ b/internal/controller/reconciler/clickhouse.go @@ -210,6 +210,18 @@ func managedClickHouseWriteState( } } + desiredServiceAccount, err := altinity.ToServiceAccount(wandb, spec, objStorage, client.Scheme()) + if err != nil { + log.Error(err, "failed to translate ClickHouse ServiceAccount") + return []metav1.Condition{ + { + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ControllerErrorReason, + }, + } + } + desired, err := altinity.ToClickHouseVendorSpec(ctx, wandb, spec, client.Scheme(), objStorage, waitForObjectStore, mfst) if err != nil { log.Error(err, "failed to translate ClickHouse spec to vendor spec") @@ -229,7 +241,7 @@ func managedClickHouseWriteState( } results := make([]metav1.Condition, 0) - results = append(results, altinity.WriteState(ctx, client, specNamespacedName, desiredKeeper, desired)...) + results = append(results, altinity.WriteState(ctx, client, specNamespacedName, desiredServiceAccount, desiredKeeper, desired)...) return results } diff --git a/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml b/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml index 09deb2e0..ff472e5c 100644 --- a/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml +++ b/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml @@ -1157,6 +1157,17 @@ spec: required: - onDelete type: object + serviceAccount: + properties: + annotations: + additionalProperties: + type: string + type: object + create: + type: boolean + serviceAccountName: + type: string + type: object storageSize: type: string telemetry: @@ -1760,6 +1771,17 @@ spec: required: - onDelete type: object + serviceAccount: + properties: + annotations: + additionalProperties: + type: string + type: object + create: + type: boolean + serviceAccountName: + type: string + type: object skipDataRecovery: type: boolean storageSize: diff --git a/internal/webhook/v2/weightsandbiases_defaulter_clickhouse_test.go b/internal/webhook/v2/weightsandbiases_defaulter_clickhouse_test.go index 65b27ce8..2250ab68 100644 --- a/internal/webhook/v2/weightsandbiases_defaulter_clickhouse_test.go +++ b/internal/webhook/v2/weightsandbiases_defaulter_clickhouse_test.go @@ -76,6 +76,9 @@ var _ = Describe("WeightsAndBiasesCustomDefaulter - ClickHouse", func() { g.Expect(defaulter.Default(ctx, wandb)).To(g.Succeed()) g.Expect(wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.Name).To(g.Equal("test-wandb-chi")) + g.Expect(wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.ServiceAccount.Create).ToNot(g.BeNil()) + g.Expect(*wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.ServiceAccount.Create).To(g.BeTrue()) + g.Expect(wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.ServiceAccount.ServiceAccountName).To(g.Equal("test-wandb-chi")) }) It("keys non-default instance names before the suffix", func() { diff --git a/internal/webhook/v2/weightsandbiases_defaulter_kafka_test.go b/internal/webhook/v2/weightsandbiases_defaulter_kafka_test.go index 74c1d862..fcc93ffe 100644 --- a/internal/webhook/v2/weightsandbiases_defaulter_kafka_test.go +++ b/internal/webhook/v2/weightsandbiases_defaulter_kafka_test.go @@ -69,5 +69,8 @@ var _ = Describe("WeightsAndBiasesCustomDefaulter - Kafka", func() { g.Expect(wandb.Spec.Kafka.ManagedKafka).ToNot(g.BeNil()) g.Expect(wandb.Spec.Kafka.ManagedKafka.Name).To(g.Equal("test-wandb-kafka")) g.Expect(wandb.Spec.Kafka.ManagedKafka.Namespace).To(g.Equal("test-namespace")) + g.Expect(wandb.Spec.Kafka.ManagedKafka.ServiceAccount.Create).ToNot(g.BeNil()) + g.Expect(*wandb.Spec.Kafka.ManagedKafka.ServiceAccount.Create).To(g.BeTrue()) + g.Expect(wandb.Spec.Kafka.ManagedKafka.ServiceAccount.ServiceAccountName).To(g.Equal("test-wandb-kafka")) }) }) diff --git a/internal/webhook/v2/weightsandbiases_webhook.go b/internal/webhook/v2/weightsandbiases_webhook.go index 6e25a321..ba934c54 100644 --- a/internal/webhook/v2/weightsandbiases_webhook.go +++ b/internal/webhook/v2/weightsandbiases_webhook.go @@ -273,6 +273,8 @@ func applyKafkaDefaults(wandb *appsv2.WeightsAndBiases) { if spec.Namespace == "" { spec.Namespace = wandb.Namespace } + + applyManagedServiceAccountDefaults(&spec.ServiceAccount, spec.Name) } func applyObjectStoreDefaults(wandb *appsv2.WeightsAndBiases) { @@ -328,10 +330,20 @@ func applyClickHouseDefaults(wandb *appsv2.WeightsAndBiases) { if spec.ManagedClickHouse.Namespace == "" { spec.ManagedClickHouse.Namespace = wandb.Namespace } + applyManagedServiceAccountDefaults(&spec.ManagedClickHouse.ServiceAccount, spec.ManagedClickHouse.Name) wandb.Spec.ClickHouse[key] = spec } } +func applyManagedServiceAccountDefaults(serviceAccount *appsv2.ManagedServiceAccountSpec, defaultName string) { + if serviceAccount.Create == nil { + serviceAccount.Create = ptr.To(true) + } + if serviceAccount.ServiceAccountName == "" { + serviceAccount.ServiceAccountName = defaultName + } +} + // validateSpec validates the (already defaulted) spec. oldWandb is nil on // create; update rules use it to skip values unchanged from the stored object. func validateSpec(_ context.Context, newWandb, oldWandb *appsv2.WeightsAndBiases) (admission.Warnings, error) { From 6f8f6a4eeb2eda942e4631ef8fdfaef7f38e018d Mon Sep 17 00:00:00 2001 From: Casey Abernathy <cabernathy@coreweave.com> Date: Mon, 27 Jul 2026 10:01:27 -0700 Subject: [PATCH 40/49] fix: Report complete W&B readiness and migration status (#294) --- api/v2/weightsandbiases_types.go | 27 ++- api/v2/zz_generated.deepcopy.go | 7 + .../apps.wandb.com_weightsandbiases.yaml | 52 ++++++ internal/controller/reconciler/mysql.go | 20 ++- internal/controller/reconciler/readiness.go | 140 ++++++++++++++++ .../controller/reconciler/readiness_test.go | 155 ++++++++++++++++++ .../controller/reconciler/reconcile_v2.go | 130 ++++++++++++--- .../apps.wandb.com_weightsandbiases.yaml | 52 ++++++ 8 files changed, 547 insertions(+), 36 deletions(-) create mode 100644 internal/controller/reconciler/readiness.go create mode 100644 internal/controller/reconciler/readiness_test.go diff --git a/api/v2/weightsandbiases_types.go b/api/v2/weightsandbiases_types.go index ca83f6a8..1655793f 100644 --- a/api/v2/weightsandbiases_types.go +++ b/api/v2/weightsandbiases_types.go @@ -34,6 +34,7 @@ import ( //+kubebuilder:printcolumn:name="Kafka",type=string,JSONPath=`.status.kafkaStatus.state` //+kubebuilder:printcolumn:name="ObjectStore",type=string,JSONPath=`.status.objectStoreStatus.default.state` //+kubebuilder:printcolumn:name="ClickHouse",type=string,JSONPath=`.status.clickhouseStatus.default.state` +//+kubebuilder:printcolumn:name="Migration",type=string,JSONPath=`.status.wandb.migration.phase` // WeightsAndBiases is the Schema for the weightsandbiases API. type WeightsAndBiases struct { @@ -730,8 +731,12 @@ type ClickHouseConfig struct { // WeightsAndBiasesStatus defines the observed state of WeightsAndBiases. type WeightsAndBiasesStatus struct { - Ready bool `json:"ready"` - Wandb WandbStatus `json:"wandb,omitempty"` + Ready bool `json:"ready"` + // Conditions includes the standard Ready condition for the current generation. + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty"` + Wandb WandbStatus `json:"wandb,omitempty"` // MySQLStatus, RedisStatus, ObjectStoreStatus and ClickHouseStatus are keyed // by instance name, mirroring the corresponding spec maps. MySQLStatus map[string]MysqlInfraStatus `json:"mysqlStatus,omitempty"` @@ -780,18 +785,24 @@ type WandbStatus struct { } type WandbMigrationStatus struct { - Version string `json:"version,omitempty"` - LastSuccessVersion string `json:"lastSuccessVersion,omitempty"` - Ready bool `json:"ready,omitempty"` - Reason string `json:"reason,omitempty"` - Jobs map[string]MigrationJobStatus `json:"jobs,omitempty"` + Version string `json:"version,omitempty"` + LastSuccessVersion string `json:"lastSuccessVersion,omitempty"` + Ready bool `json:"ready,omitempty"` + // Phase is Running, Failed, Succeeded, or Unknown. + Phase string `json:"phase,omitempty"` + Reason string `json:"reason,omitempty"` + Jobs map[string]MigrationJobStatus `json:"jobs,omitempty"` } type MigrationJobStatus struct { Name string `json:"name,omitempty"` Succeeded bool `json:"succeeded,omitempty"` Failed bool `json:"failed,omitempty"` - Message string `json:"message,omitempty"` + // Phase is Running, Failed, Succeeded, or Unknown. + Phase string `json:"phase,omitempty"` + // Reason is copied from the terminal Kubernetes Job condition when present. + Reason string `json:"reason,omitempty"` + Message string `json:"message,omitempty"` } type WBInfraStatus struct { diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index e1ebfcdd..76ab543b 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -1746,6 +1746,13 @@ func (in *WeightsAndBiasesSpec) DeepCopy() *WeightsAndBiasesSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WeightsAndBiasesStatus) DeepCopyInto(out *WeightsAndBiasesStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } in.Wandb.DeepCopyInto(&out.Wandb) if in.MySQLStatus != nil { in, out := &in.MySQLStatus, &out.MySQLStatus diff --git a/config/crd/bases/apps.wandb.com_weightsandbiases.yaml b/config/crd/bases/apps.wandb.com_weightsandbiases.yaml index ff472e5c..4ee11815 100644 --- a/config/crd/bases/apps.wandb.com_weightsandbiases.yaml +++ b/config/crd/bases/apps.wandb.com_weightsandbiases.yaml @@ -66,6 +66,9 @@ spec: - jsonPath: .status.clickhouseStatus.default.state name: ClickHouse type: string + - jsonPath: .status.wandb.migration.phase + name: Migration + type: string name: v2 schema: openAPIV3Schema: @@ -4548,6 +4551,45 @@ spec: - ready type: object type: object + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map gatewayStatus: properties: addresses: @@ -6274,12 +6316,18 @@ spec: type: string name: type: string + phase: + type: string + reason: + type: string succeeded: type: boolean type: object type: object lastSuccessVersion: type: string + phase: + type: string ready: type: boolean reason: @@ -6296,6 +6344,10 @@ spec: type: string name: type: string + phase: + type: string + reason: + type: string succeeded: type: boolean type: object diff --git a/internal/controller/reconciler/mysql.go b/internal/controller/reconciler/mysql.go index d5e5faf2..a078e521 100644 --- a/internal/controller/reconciler/mysql.go +++ b/internal/controller/reconciler/mysql.go @@ -428,7 +428,11 @@ func runMysqlInitJobInstance(ctx context.Context, client client.Client, wandb *a return ctrl.Result{}, err } - wandb.Status.Wandb.MySQLInit[key] = apiv2.MigrationJobStatus{Name: jobName, Succeeded: false} + wandb.Status.Wandb.MySQLInit[key] = apiv2.MigrationJobStatus{ + Name: jobName, + Phase: migrationPhaseRunning, + Reason: "JobCreated", + } if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { return ctrl.Result{}, err } @@ -438,7 +442,12 @@ func runMysqlInitJobInstance(ctx context.Context, client client.Client, wandb *a if job.Status.Succeeded > 0 { logger.Info("MySQL init job succeeded") - wandb.Status.Wandb.MySQLInit[key] = apiv2.MigrationJobStatus{Name: jobName, Succeeded: true} + wandb.Status.Wandb.MySQLInit[key] = apiv2.MigrationJobStatus{ + Name: jobName, + Succeeded: true, + Phase: migrationPhaseSucceeded, + Reason: "JobSucceeded", + } if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { return ctrl.Result{}, err } @@ -447,7 +456,12 @@ func runMysqlInitJobInstance(ctx context.Context, client client.Client, wandb *a if job.Status.Failed > 0 { logger.Info("MySQL init job failed") - wandb.Status.Wandb.MySQLInit[key] = apiv2.MigrationJobStatus{Name: jobName, Failed: true} + wandb.Status.Wandb.MySQLInit[key] = apiv2.MigrationJobStatus{ + Name: jobName, + Failed: true, + Phase: migrationPhaseFailed, + Reason: "JobFailed", + } if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { return ctrl.Result{}, err } diff --git a/internal/controller/reconciler/readiness.go b/internal/controller/reconciler/readiness.go new file mode 100644 index 00000000..b7ef38d5 --- /dev/null +++ b/internal/controller/reconciler/readiness.go @@ -0,0 +1,140 @@ +package reconciler + +import ( + "context" + "fmt" + "sort" + "strings" + + apiv2 "github.com/wandb/operator/api/v2" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + readyConditionType = "Ready" + + migrationPhaseRunning = "Running" + migrationPhaseFailed = "Failed" + migrationPhaseSucceeded = "Succeeded" + migrationPhaseUnknown = "Unknown" +) + +func setReadyStatus(wandb *apiv2.WeightsAndBiases, ready bool, reason, message string) { + wandb.Status.Ready = ready + status := metav1.ConditionFalse + if ready { + status = metav1.ConditionTrue + } + apimeta.SetStatusCondition(&wandb.Status.Conditions, metav1.Condition{ + Type: readyConditionType, + Status: status, + ObservedGeneration: wandb.Generation, + Reason: reason, + Message: message, + }) +} + +func updateReadyStatus( + ctx context.Context, + c ctrlclient.Client, + wandb *apiv2.WeightsAndBiases, + statusBefore apiv2.WeightsAndBiasesStatus, + ready bool, + reason string, + message string, +) error { + setReadyStatus(wandb, ready, reason, message) + return updateWandbStatusIfChanged(ctx, c, wandb, statusBefore) +} + +func infrastructureBlockers(wandb *apiv2.WeightsAndBiases) []string { + var blockers []string + for key := range wandb.Spec.Redis { + if !wandb.Status.RedisStatus[key].Ready { + blockers = append(blockers, "redis/"+key) + } + } + for key := range wandb.Spec.MySQL { + if !wandb.Status.MySQLStatus[key].Ready { + blockers = append(blockers, "mysql/"+key) + } + } + if wandb.Spec.Kafka.ManagedKafka != nil && !wandb.Status.KafkaStatus.Ready { + blockers = append(blockers, "kafka") + } + for key := range wandb.Spec.ObjectStore { + if !wandb.Status.ObjectStoreStatus[key].Ready { + blockers = append(blockers, "objectStore/"+key) + } + } + for key := range wandb.Spec.ClickHouse { + if !wandb.Status.ClickHouseStatus[key].Ready { + blockers = append(blockers, "clickhouse/"+key) + } + } + sort.Strings(blockers) + return blockers +} + +func mysqlInitializationReadiness(wandb *apiv2.WeightsAndBiases) (string, string) { + var failed []string + var pending []string + for key, spec := range wandb.Spec.MySQL { + if spec.ManagedMysql == nil { + continue + } + status := wandb.Status.Wandb.MySQLInit[key] + switch { + case status.Succeeded: + case status.Failed: + failed = append(failed, key) + default: + pending = append(pending, key) + } + } + sort.Strings(failed) + sort.Strings(pending) + if len(failed) > 0 { + return "MySQLInitializationFailed", "MySQL initialization jobs failed: " + strings.Join(failed, ", ") + } + return "MySQLInitializationPending", "waiting for MySQL initialization jobs: " + strings.Join(pending, ", ") +} + +func migrationReadiness(wandb *apiv2.WeightsAndBiases) (string, string) { + status := wandb.Status.Wandb.Migration + if status.Phase != migrationPhaseFailed && status.Reason != migrationPhaseFailed { + phase := status.Phase + if phase == "" { + phase = status.Reason + } + if phase == "" { + phase = migrationPhaseUnknown + } + return "MigrationPending", fmt.Sprintf("migration phase is %s for version %s", phase, status.Version) + } + + var failures []string + for name, job := range status.Jobs { + if !job.Failed && job.Phase != migrationPhaseFailed { + continue + } + detail := job.Name + if detail == "" { + detail = name + } + switch { + case job.Message != "": + detail += ": " + job.Message + case job.Reason != "": + detail += ": " + job.Reason + } + failures = append(failures, detail) + } + sort.Strings(failures) + if len(failures) == 0 { + return "MigrationFailed", "one or more migration jobs failed" + } + return "MigrationFailed", "migration jobs failed: " + strings.Join(failures, "; ") +} diff --git a/internal/controller/reconciler/readiness_test.go b/internal/controller/reconciler/readiness_test.go new file mode 100644 index 00000000..6b771b24 --- /dev/null +++ b/internal/controller/reconciler/readiness_test.go @@ -0,0 +1,155 @@ +package reconciler + +import ( + "context" + "testing" + + apiv2 "github.com/wandb/operator/api/v2" + servermanifest "github.com/wandb/operator/pkg/wandb/manifest" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestInferStateBlocksOnExternalInfrastructure(t *testing.T) { + scheme := runtime.NewScheme() + if err := apiv2.AddToScheme(scheme); err != nil { + t.Fatalf("add W&B API to scheme: %v", err) + } + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default", Generation: 2}, + Spec: apiv2.WeightsAndBiasesSpec{ + Redis: map[string]apiv2.RedisSpec{ + apiv2.DefaultInstanceName: {ExternalRedis: &apiv2.RedisConnection{}}, + }, + }, + Status: apiv2.WeightsAndBiasesStatus{ + Ready: true, + ObservedGeneration: 2, + RedisStatus: map[string]apiv2.RedisInfraStatus{ + apiv2.DefaultInstanceName: {}, + }, + }, + } + c := fake.NewClientBuilder(). + WithScheme(scheme). + WithStatusSubresource(&apiv2.WeightsAndBiases{}). + WithObjects(wandb). + Build() + + if err := inferState(context.Background(), c, wandb); err != nil { + t.Fatalf("infer state: %v", err) + } + + actual := &apiv2.WeightsAndBiases{} + if err := c.Get(context.Background(), client.ObjectKeyFromObject(wandb), actual); err != nil { + t.Fatalf("get updated W&B resource: %v", err) + } + if actual.Status.Ready { + t.Fatal("external Redis must block overall readiness") + } + condition := apimeta.FindStatusCondition(actual.Status.Conditions, readyConditionType) + if condition == nil { + t.Fatal("Ready condition was not written") + } + if condition.Status != metav1.ConditionFalse || condition.Reason != "DependenciesNotReady" { + t.Fatalf("unexpected Ready condition: %#v", condition) + } + if condition.ObservedGeneration != 2 { + t.Fatalf("observed generation = %d, want 2", condition.ObservedGeneration) + } +} + +func TestSetReadyStatusKeepsBooleanAndConditionConsistent(t *testing.T) { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Generation: 4}, + } + + setReadyStatus(wandb, true, "ReconciliationSucceeded", "ready") + + if !wandb.Status.Ready { + t.Fatal("status.ready was not set") + } + condition := apimeta.FindStatusCondition(wandb.Status.Conditions, readyConditionType) + if condition == nil || condition.Status != metav1.ConditionTrue { + t.Fatalf("unexpected Ready condition: %#v", condition) + } + if condition.ObservedGeneration != 4 { + t.Fatalf("observed generation = %d, want 4", condition.ObservedGeneration) + } +} + +func TestRunMigrationsSurfacesFailedJobPhaseAndReason(t *testing.T) { + scheme := runtime.NewScheme() + if err := apiv2.AddToScheme(scheme); err != nil { + t.Fatalf("add W&B API to scheme: %v", err) + } + if err := batchv1.AddToScheme(scheme); err != nil { + t.Fatalf("add batch API to scheme: %v", err) + } + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default"}, + Spec: apiv2.WeightsAndBiasesSpec{ + Wandb: apiv2.WandbAppSpec{Version: "0.82.2"}, + }, + Status: apiv2.WeightsAndBiasesStatus{ + Wandb: apiv2.WandbStatus{ + Migration: apiv2.WandbMigrationStatus{ + Version: "0.82.2", + Jobs: map[string]apiv2.MigrationJobStatus{}, + }, + }, + }, + } + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb-weave-trace", Namespace: "default"}, + Status: batchv1.JobStatus{ + Conditions: []batchv1.JobCondition{{ + Type: batchv1.JobFailed, + Status: corev1.ConditionTrue, + Reason: "BackoffLimitExceeded", + Message: "migration exited after detecting a partial version", + }}, + }, + } + c := fake.NewClientBuilder(). + WithScheme(scheme). + WithStatusSubresource(&apiv2.WeightsAndBiases{}). + WithObjects(wandb, job). + Build() + manifest := servermanifest.Manifest{ + Migrations: map[string]servermanifest.MigrationJob{ + "weave-trace": {}, + }, + } + + result, err := runMigrations(context.Background(), c, wandb, manifest) + if err != nil { + t.Fatalf("run migrations: %v", err) + } + if result.RequeueAfter == 0 { + t.Fatal("failed migration should requeue") + } + if wandb.Status.Wandb.Migration.Phase != migrationPhaseFailed { + t.Fatalf("migration phase = %q, want %q", wandb.Status.Wandb.Migration.Phase, migrationPhaseFailed) + } + jobStatus := wandb.Status.Wandb.Migration.Jobs["weave-trace"] + if jobStatus.Phase != migrationPhaseFailed || jobStatus.Reason != "BackoffLimitExceeded" { + t.Fatalf("unexpected migration job status: %#v", jobStatus) + } + if jobStatus.Message == "" { + t.Fatal("migration failure message was not surfaced") + } + + reason, message := migrationReadiness(wandb) + if reason != "MigrationFailed" { + t.Fatalf("readiness reason = %q, want MigrationFailed", reason) + } + if message == "" { + t.Fatal("readiness message should identify the failed migration") + } +} diff --git a/internal/controller/reconciler/reconcile_v2.go b/internal/controller/reconciler/reconcile_v2.go index 38234bac..57891d89 100644 --- a/internal/controller/reconciler/reconcile_v2.go +++ b/internal/controller/reconciler/reconcile_v2.go @@ -423,6 +423,18 @@ func ReconcileWandbManifest( if !redisReady || !mysqlReady || !kafkaReady || !objectStoreReady || !clickHouseReady { logger.Info("Infra components not ready yet, requeuing for reconciliation", "redis", redisReady, "moco", mysqlReady, "kafka", kafkaReady, "objectStore", objectStoreReady, "clickhouse", clickHouseReady) + blockers := infrastructureBlockers(wandb) + if err := updateReadyStatus( + ctx, + client, + wandb, + statusBefore, + false, + "DependenciesNotReady", + "dependencies not ready: "+strings.Join(blockers, ", "), + ); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{RequeueAfter: defaultRequeueDuration}, nil } @@ -447,6 +459,10 @@ func ReconcileWandbManifest( if !allMysqlInitSucceeded(wandb) { logger.Info("Mysql init not yet successful") + reason, message := mysqlInitializationReadiness(wandb) + if err := updateReadyStatus(ctx, client, wandb, statusBefore, false, reason, message); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{RequeueAfter: 5 * time.Second}, nil } @@ -495,6 +511,10 @@ func ReconcileWandbManifest( if !wandb.Status.Wandb.Migration.Ready { logger.Info("Migration not yet successful for version", "version", wandb.Spec.Wandb.Version, "reason", wandb.Status.Wandb.Migration.Reason) + reason, message := migrationReadiness(wandb) + if err := updateReadyStatus(ctx, client, wandb, statusBefore, false, reason, message); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{RequeueAfter: 5 * time.Second}, nil } @@ -506,7 +526,8 @@ func ReconcileWandbManifest( // Gate on live Deployment readiness, not status.wandb.applications: the // copied status map can be a stale snapshot (it only refreshes when this // reconciler runs), and a frozen mid-rollout entry would block cleanup forever. - if healthy, notReady := deploymentsHealthy(ctx, client, wandb.Namespace, buildDesiredAppNames(manifest)); healthy { + applicationsHealthy, notReady := deploymentsHealthy(ctx, client, wandb.Namespace, buildDesiredAppNames(manifest)) + if applicationsHealthy { if err := cleanupLegacyV1Deployments(ctx, client, wandb); err != nil { logger.Error(err, "Failed to clean up legacy v1 deployments") return ctrl.Result{}, err @@ -523,11 +544,26 @@ func ReconcileWandbManifest( } } + if applicationsHealthy { + setReadyStatus( + wandb, + true, + "ReconciliationSucceeded", + "all dependencies, migrations, and application deployments are ready", + ) + } else { + message := "waiting for application deployments: " + strings.Join(notReady, ", ") + if len(notReady) == 0 { + message = "no desired application deployments were found" + } + setReadyStatus(wandb, false, "ApplicationsNotReady", message) + } + if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { return ctrl.Result{}, err } - return ctrl.Result{}, nil + return result, nil } func reconcileApplications( @@ -1141,6 +1177,17 @@ func runMigrations(ctx context.Context, client ctrlClient.Client, wandb *apiv2.W version := wandb.Spec.Wandb.Version if wandb.Status.Wandb.Migration.Ready && wandb.Status.Wandb.Migration.Version == version { + wandb.Status.Wandb.Migration.Phase = migrationPhaseSucceeded + if wandb.Status.Wandb.Migration.Reason == "" { + wandb.Status.Wandb.Migration.Reason = "Complete" + } + for name, jobStatus := range wandb.Status.Wandb.Migration.Jobs { + if jobStatus.Succeeded && jobStatus.Phase == "" { + jobStatus.Phase = migrationPhaseSucceeded + jobStatus.Reason = "JobSucceeded" + wandb.Status.Wandb.Migration.Jobs[name] = jobStatus + } + } for name := range manifest.Migrations { jobName := fmt.Sprintf("%s-%s", wandb.Name, name) job := &batchv1.Job{ @@ -1158,12 +1205,16 @@ func runMigrations(ctx context.Context, client ctrlClient.Client, wandb *apiv2.W } } } + if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, nil } if wandb.Status.Wandb.Migration.Version != version { wandb.Status.Wandb.Migration.Version = version wandb.Status.Wandb.Migration.Ready = false + wandb.Status.Wandb.Migration.Phase = migrationPhaseRunning wandb.Status.Wandb.Migration.Reason = "Running" wandb.Status.Wandb.Migration.Jobs = make(map[string]apiv2.MigrationJobStatus) if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { @@ -1174,6 +1225,7 @@ func runMigrations(ctx context.Context, client ctrlClient.Client, wandb *apiv2.W if len(manifest.Migrations) == 0 { wandb.Status.Wandb.Migration.Ready = true + wandb.Status.Wandb.Migration.Phase = migrationPhaseSucceeded wandb.Status.Wandb.Migration.Reason = "Complete" wandb.Status.Wandb.Migration.LastSuccessVersion = version if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { @@ -1271,7 +1323,10 @@ func runMigrations(ctx context.Context, client ctrlClient.Client, wandb *apiv2.W } jobStatus.Succeeded = false + jobStatus.Phase = migrationPhaseRunning + jobStatus.Reason = "JobCreated" wandb.Status.Wandb.Migration.Jobs[name] = jobStatus + wandb.Status.Wandb.Migration.Phase = migrationPhaseRunning wandb.Status.Wandb.Migration.Reason = "Running" wandb.Status.Wandb.Migration.Ready = false if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { @@ -1283,11 +1338,27 @@ func runMigrations(ctx context.Context, client ctrlClient.Client, wandb *apiv2.W if job.Status.Succeeded > 0 { jobStatus.Succeeded = true + jobStatus.Phase = migrationPhaseSucceeded + jobStatus.Reason = "JobSucceeded" + for _, cond := range job.Status.Conditions { + if cond.Type == batchv1.JobComplete && cond.Status == corev1.ConditionTrue { + if cond.Reason != "" { + jobStatus.Reason = cond.Reason + } + jobStatus.Message = cond.Message + break + } + } } else { allSucceeded = false for _, cond := range job.Status.Conditions { if cond.Type == batchv1.JobFailed && cond.Status == corev1.ConditionTrue { jobStatus.Failed = true + jobStatus.Phase = migrationPhaseFailed + jobStatus.Reason = cond.Reason + if jobStatus.Reason == "" { + jobStatus.Reason = "JobFailed" + } jobStatus.Message = cond.Message anyFailed = true break @@ -1295,6 +1366,12 @@ func runMigrations(ctx context.Context, client ctrlClient.Client, wandb *apiv2.W } if !jobStatus.Failed { anyRunning = true + jobStatus.Phase = migrationPhaseRunning + if job.Status.Active > 0 { + jobStatus.Reason = "JobRunning" + } else { + jobStatus.Reason = "JobPending" + } } } @@ -1302,18 +1379,22 @@ func runMigrations(ctx context.Context, client ctrlClient.Client, wandb *apiv2.W } if anyFailed { + wandb.Status.Wandb.Migration.Phase = migrationPhaseFailed wandb.Status.Wandb.Migration.Reason = "Failed" wandb.Status.Wandb.Migration.Ready = false } else if anyRunning || !allSucceeded { + wandb.Status.Wandb.Migration.Phase = migrationPhaseRunning wandb.Status.Wandb.Migration.Reason = "Running" wandb.Status.Wandb.Migration.Ready = false } else if allSucceeded { + wandb.Status.Wandb.Migration.Phase = migrationPhaseSucceeded wandb.Status.Wandb.Migration.Reason = "Complete" wandb.Status.Wandb.Migration.Ready = true if wandb.Status.Wandb.Migration.LastSuccessVersion != version { wandb.Status.Wandb.Migration.LastSuccessVersion = version } } else { + wandb.Status.Wandb.Migration.Phase = migrationPhaseUnknown wandb.Status.Wandb.Migration.Reason = "Unknown" wandb.Status.Wandb.Migration.Ready = false } @@ -1477,19 +1558,6 @@ func resolveCRFieldSecretSelector(obj any, path string) (corev1.SecretKeySelecto return sel, true } -// managedInstancesReady reports whether every managed instance has a ready -// status. External and absent instances are treated as ready, matching the -// pre-multi-instance behavior where only operator-managed infra gated overall -// readiness. -func managedInstancesReady[S any, T any](specs map[string]S, statuses map[string]T, isManaged func(S) bool, ready func(T) bool) bool { - for key, spec := range specs { - if isManaged(spec) && !ready(statuses[key]) { - return false - } - } - return true -} - // allInstancesReady reports whether every instance (managed or external) has a // ready status. func allInstancesReady[S any, T any](specs map[string]S, statuses map[string]T, ready func(T) bool) bool { @@ -1523,16 +1591,28 @@ func inferState( log := ctrl.LoggerFrom(ctx) statusBefore := wandb.DeepCopy().Status - redisOk := managedInstancesReady(wandb.Spec.Redis, wandb.Status.RedisStatus, func(s apiv2.RedisSpec) bool { return s.ManagedRedis != nil }, func(s apiv2.RedisInfraStatus) bool { return s.Ready }) - objectStoreOk := managedInstancesReady(wandb.Spec.ObjectStore, wandb.Status.ObjectStoreStatus, func(s apiv2.ObjectStoreSpec) bool { return s.ManagedObjectStore != nil }, func(s apiv2.ObjectStoreInfraStatus) bool { return s.Ready }) - mysqlOk := managedInstancesReady(wandb.Spec.MySQL, wandb.Status.MySQLStatus, func(s apiv2.MySQLSpec) bool { return s.ManagedMysql != nil }, func(s apiv2.MysqlInfraStatus) bool { return s.Ready }) - clickHouseOk := managedInstancesReady(wandb.Spec.ClickHouse, wandb.Status.ClickHouseStatus, func(s apiv2.ClickHouseSpec) bool { return s.ManagedClickHouse != nil }, func(s apiv2.ClickHouseInfraStatus) bool { return s.Ready }) - kafkaOk := wandb.Spec.Kafka.ManagedKafka == nil || wandb.Status.KafkaStatus.Ready - - if redisOk && objectStoreOk && mysqlOk && clickHouseOk && kafkaOk { - wandb.Status.Ready = true - } else { - wandb.Status.Ready = false + blockers := infrastructureBlockers(wandb) + switch { + case len(blockers) > 0: + setReadyStatus( + wandb, + false, + "DependenciesNotReady", + "dependencies not ready: "+strings.Join(blockers, ", "), + ) + case wandb.Status.ObservedGeneration != wandb.Generation: + setReadyStatus( + wandb, + false, + "Reconciling", + fmt.Sprintf( + "observed generation %d does not match generation %d", + wandb.Status.ObservedGeneration, + wandb.Generation, + ), + ) + default: + return nil } if err := updateWandbStatusIfChanged(ctx, client, wandb, statusBefore); err != nil { diff --git a/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml b/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml index ff472e5c..4ee11815 100644 --- a/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml +++ b/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml @@ -66,6 +66,9 @@ spec: - jsonPath: .status.clickhouseStatus.default.state name: ClickHouse type: string + - jsonPath: .status.wandb.migration.phase + name: Migration + type: string name: v2 schema: openAPIV3Schema: @@ -4548,6 +4551,45 @@ spec: - ready type: object type: object + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + 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])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map gatewayStatus: properties: addresses: @@ -6274,12 +6316,18 @@ spec: type: string name: type: string + phase: + type: string + reason: + type: string succeeded: type: boolean type: object type: object lastSuccessVersion: type: string + phase: + type: string ready: type: boolean reason: @@ -6296,6 +6344,10 @@ spec: type: string name: type: string + phase: + type: string + reason: + type: string succeeded: type: boolean type: object From f88c6dd5ae49246c65e2fcd6627d9432b7bd1f74 Mon Sep 17 00:00:00 2001 From: Casey Abernathy <cabernathy@coreweave.com> Date: Mon, 27 Jul 2026 13:54:55 -0700 Subject: [PATCH 41/49] fix: Reject incomplete external Redis connections (#292) --- internal/controller/infra/external/common.go | 31 ++++++-- .../controller/infra/external/common_test.go | 52 ++++++++++++++ .../controller/infra/external/redis/redis.go | 37 +++++++++- .../infra/external/redis/redis_test.go | 70 +++++++++++++++---- .../webhook/v2/weightsandbiases_webhook.go | 27 +++++-- .../v2/weightsandbiases_webhook_test.go | 54 +++++++++++++- 6 files changed, 245 insertions(+), 26 deletions(-) create mode 100644 internal/controller/infra/external/common_test.go diff --git a/internal/controller/infra/external/common.go b/internal/controller/infra/external/common.go index 280d0ad7..1fc7cc21 100644 --- a/internal/controller/infra/external/common.go +++ b/internal/controller/infra/external/common.go @@ -135,11 +135,19 @@ func InferExternalStatus( generation int64, hasConnection bool, ) (string, bool, []metav1.Condition) { - state := common.HealthyState - ready := true - if !hasConnection { - state = common.ErrorState - ready = false + hasCurrentReconciledCondition := false + for _, condition := range newConditions { + if condition.Type == common.ReconciledType { + hasCurrentReconciledCondition = true + break + } + } + if hasConnection && !hasCurrentReconciledCondition { + newConditions = append(newConditions, metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionTrue, + Reason: common.ResourceExistsReason, + }) } updatedConditions := common.ComputeConditionUpdates( @@ -148,6 +156,19 @@ func InferExternalStatus( generation, common.DefaultConditionExpiry, ) + + ready := hasConnection + for _, condition := range newConditions { + if condition.Type == common.ReconciledType && condition.Status != metav1.ConditionTrue { + ready = false + break + } + } + + state := common.HealthyState + if !ready { + state = common.ErrorState + } return state, ready, updatedConditions } diff --git a/internal/controller/infra/external/common_test.go b/internal/controller/infra/external/common_test.go new file mode 100644 index 00000000..a5ef26d7 --- /dev/null +++ b/internal/controller/infra/external/common_test.go @@ -0,0 +1,52 @@ +package external + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/wandb/operator/internal/controller/common" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestInferExternalStatusUsesCurrentReconcileCondition(t *testing.T) { + failed := metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ResourceErrorReason, + Message: "invalid connection", + } + + state, ready, conditions := InferExternalStatus(nil, []metav1.Condition{failed}, 2, true) + + require.Equal(t, common.ErrorState, state) + require.False(t, ready) + require.Len(t, conditions, 1) + require.Equal(t, metav1.ConditionFalse, conditions[0].Status) + require.Equal(t, int64(2), conditions[0].ObservedGeneration) +} + +func TestInferExternalStatusClearsRecoveredFailure(t *testing.T) { + old := metav1.Condition{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ResourceErrorReason, + Message: "invalid connection", + ObservedGeneration: 1, + } + + state, ready, conditions := InferExternalStatus([]metav1.Condition{old}, nil, 2, true) + + require.Equal(t, common.HealthyState, state) + require.True(t, ready) + require.Len(t, conditions, 1) + require.Equal(t, metav1.ConditionTrue, conditions[0].Status) + require.Equal(t, common.ResourceExistsReason, conditions[0].Reason) + require.Equal(t, int64(2), conditions[0].ObservedGeneration) +} + +func TestInferExternalStatusRequiresConnection(t *testing.T) { + state, ready, _ := InferExternalStatus(nil, nil, 1, false) + + require.Equal(t, common.ErrorState, state) + require.False(t, ready) +} diff --git a/internal/controller/infra/external/redis/redis.go b/internal/controller/infra/external/redis/redis.go index 2bb07762..be9193e1 100644 --- a/internal/controller/infra/external/redis/redis.go +++ b/internal/controller/infra/external/redis/redis.go @@ -4,8 +4,11 @@ import ( "context" "fmt" "net/url" + "strconv" + "strings" apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" "github.com/wandb/operator/internal/controller/infra/external" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -46,9 +49,20 @@ func WriteState( if err != nil { logger.Error(err, "failed to resolve external redis fields") return []metav1.Condition{{ - Type: "Reconciled", - Status: metav1.ConditionFalse, - Reason: "ApiError", + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ApiErrorReason, + Message: err.Error(), + }} + } + + if err := validateConnectionData(data); err != nil { + logger.Error(err, "invalid external redis connection") + return []metav1.Condition{{ + Type: common.ReconciledType, + Status: metav1.ConditionFalse, + Reason: common.ResourceErrorReason, + Message: err.Error(), }} } @@ -81,6 +95,23 @@ func WriteState( return external.WriteConnectionSecret(ctx, c, wandb, nsName, data) } +func validateConnectionData(data map[string]string) error { + host := strings.TrimSpace(data["Host"]) + if host == "" { + return fmt.Errorf("external Redis host is empty") + } + + portValue := strings.TrimSpace(data["Port"]) + port, err := strconv.Atoi(portValue) + if err != nil || port < 1 || port > 65535 { + return fmt.Errorf("external Redis port %q must be an integer between 1 and 65535", portValue) + } + + data["Host"] = host + data["Port"] = strconv.Itoa(port) + return nil +} + func ReadState( ctx context.Context, c client.Client, diff --git a/internal/controller/infra/external/redis/redis_test.go b/internal/controller/infra/external/redis/redis_test.go index ba789e7a..8a694bc8 100644 --- a/internal/controller/infra/external/redis/redis_test.go +++ b/internal/controller/infra/external/redis/redis_test.go @@ -7,10 +7,13 @@ import ( "github.com/stretchr/testify/require" apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" ) @@ -23,19 +26,15 @@ func redisSel(key string) corev1.SecretKeySelector { } } -func TestWriteStateAddsCACertPathAndTLSWhenCACertPresent(t *testing.T) { +func redisWriteStateFixture(t *testing.T, sourceData map[string][]byte) (ctrlclient.Client, *apiv2.WeightsAndBiases) { + t.Helper() scheme := runtime.NewScheme() require.NoError(t, corev1.AddToScheme(scheme)) require.NoError(t, apiv2.AddToScheme(scheme)) source := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: redisSourceSecretName, Namespace: "default"}, - Data: map[string][]byte{ - "Host": []byte("redis.example.com"), - "Port": []byte("6379"), - "Password": []byte("secret"), - "SslCa": []byte("---ca---"), - }, + Data: sourceData, } wandb := &apiv2.WeightsAndBiases{ TypeMeta: metav1.TypeMeta{APIVersion: "apps.wandb.com/v2", Kind: "WeightsAndBiases"}, @@ -43,15 +42,32 @@ func TestWriteStateAddsCACertPathAndTLSWhenCACertPresent(t *testing.T) { Spec: apiv2.WeightsAndBiasesSpec{ Redis: map[string]apiv2.RedisSpec{apiv2.DefaultInstanceName: { ExternalRedis: &apiv2.RedisConnection{ - Host: redisSel("Host"), - Port: redisSel("Port"), - Password: redisSel("Password"), - SslCa: redisSel("SslCa"), + Host: redisSel("Host"), + Port: redisSel("Port"), }, }}, }, } - client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(wandb, source).Build() + if _, ok := sourceData["Password"]; ok { + connection := wandb.Spec.Redis[apiv2.DefaultInstanceName] + connection.ExternalRedis.Password = redisSel("Password") + wandb.Spec.Redis[apiv2.DefaultInstanceName] = connection + } + if _, ok := sourceData["SslCa"]; ok { + connection := wandb.Spec.Redis[apiv2.DefaultInstanceName] + connection.ExternalRedis.SslCa = redisSel("SslCa") + wandb.Spec.Redis[apiv2.DefaultInstanceName] = connection + } + return fake.NewClientBuilder().WithScheme(scheme).WithObjects(wandb, source).Build(), wandb +} + +func TestWriteStateAddsCACertPathAndTLSWhenCACertPresent(t *testing.T) { + client, wandb := redisWriteStateFixture(t, map[string][]byte{ + "Host": []byte("redis.example.com"), + "Port": []byte("6379"), + "Password": []byte("secret"), + "SslCa": []byte("---ca---"), + }) conditions := WriteState(context.Background(), client, wandb, apiv2.DefaultInstanceName, wandb.Spec.Redis[apiv2.DefaultInstanceName].ExternalRedis) require.Nil(t, conditions) @@ -67,6 +83,36 @@ func TestWriteStateAddsCACertPathAndTLSWhenCACertPresent(t *testing.T) { require.Equal(t, caCertPath, parsed.Query().Get("caCertPath")) } +func TestWriteStateRejectsInvalidRequiredFields(t *testing.T) { + tests := []struct { + name string + data map[string][]byte + }{ + {name: "empty host", data: map[string][]byte{"Host": {}, "Port": []byte("6379")}}, + {name: "empty port", data: map[string][]byte{"Host": []byte("redis.example.com"), "Port": {}}}, + {name: "non-numeric port", data: map[string][]byte{"Host": []byte("redis.example.com"), "Port": []byte("redis")}}, + {name: "zero port", data: map[string][]byte{"Host": []byte("redis.example.com"), "Port": []byte("0")}}, + {name: "port above range", data: map[string][]byte{"Host": []byte("redis.example.com"), "Port": []byte("65536")}}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + client, wandb := redisWriteStateFixture(t, test.data) + + conditions := WriteState(context.Background(), client, wandb, apiv2.DefaultInstanceName, wandb.Spec.Redis[apiv2.DefaultInstanceName].ExternalRedis) + + require.Len(t, conditions, 1) + require.Equal(t, common.ReconciledType, conditions[0].Type) + require.Equal(t, metav1.ConditionFalse, conditions[0].Status) + require.Equal(t, common.ResourceErrorReason, conditions[0].Reason) + require.NotEmpty(t, conditions[0].Message) + + err := client.Get(context.Background(), types.NamespacedName{Name: ConnectionSecretName, Namespace: "default"}, &corev1.Secret{}) + require.True(t, apierrors.IsNotFound(err)) + }) + } +} + func redisConnectionData(secret *corev1.Secret) map[string]string { out := map[string]string{} for k, v := range secret.Data { diff --git a/internal/webhook/v2/weightsandbiases_webhook.go b/internal/webhook/v2/weightsandbiases_webhook.go index ba934c54..fd229ca4 100644 --- a/internal/webhook/v2/weightsandbiases_webhook.go +++ b/internal/webhook/v2/weightsandbiases_webhook.go @@ -483,6 +483,7 @@ func validateMySQLSpec(wandb *appsv2.WeightsAndBiases) field.ErrorList { func validateRedisSpec(wandb *appsv2.WeightsAndBiases) field.ErrorList { var errors field.ErrorList redisPath := field.NewPath("spec").Child("redis") + _, hasPendingLegacyRedis := wandb.Annotations[v1.RedisPendingAnnotation] errors = append(errors, validateHasDefaultInstance(wandb.Spec.Redis, redisPath)...) @@ -496,16 +497,21 @@ func validateRedisSpec(wandb *appsv2.WeightsAndBiases) field.ErrorList { )) } - managed := spec.ManagedRedis - if managed == nil { + if externalRedis := spec.ExternalRedis; externalRedis != nil && !hasPendingLegacyRedis { + externalPath := instancePath.Child("externalRedis") + errors = append(errors, validateRequiredSecretSelector(externalRedis.Host, externalPath.Child("host"))...) + errors = append(errors, validateRequiredSecretSelector(externalRedis.Port, externalPath.Child("port"))...) + } + + if spec.ManagedRedis == nil { continue } - if managed.StorageSize != "" { - if _, err := resource.ParseQuantity(managed.StorageSize); err != nil { + if spec.ManagedRedis.StorageSize != "" { + if _, err := resource.ParseQuantity(spec.ManagedRedis.StorageSize); err != nil { errors = append(errors, field.Invalid( instancePath.Child("managedRedis").Child("storageSize"), - managed.StorageSize, + spec.ManagedRedis.StorageSize, "must be a valid resource quantity (e.g., '10Gi')", )) } @@ -515,6 +521,17 @@ func validateRedisSpec(wandb *appsv2.WeightsAndBiases) field.ErrorList { return errors } +func validateRequiredSecretSelector(selector corev1.SecretKeySelector, path *field.Path) field.ErrorList { + var errors field.ErrorList + if selector.Name == "" { + errors = append(errors, field.Required(path.Child("name"), "secret name is required")) + } + if selector.Key == "" { + errors = append(errors, field.Required(path.Child("key"), "secret key is required")) + } + return errors +} + func validateObjectStoreSpec(wandb *appsv2.WeightsAndBiases) field.ErrorList { var errors field.ErrorList objectStorePath := field.NewPath("spec").Child("objectStore") diff --git a/internal/webhook/v2/weightsandbiases_webhook_test.go b/internal/webhook/v2/weightsandbiases_webhook_test.go index 96fde30a..8bb0d2ae 100644 --- a/internal/webhook/v2/weightsandbiases_webhook_test.go +++ b/internal/webhook/v2/weightsandbiases_webhook_test.go @@ -22,6 +22,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + appsv1 "github.com/wandb/operator/api/v1" appsv2 "github.com/wandb/operator/api/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -122,6 +123,49 @@ var _ = Describe("WeightsAndBiases Webhook", func() { Context("When creating or updating WeightsAndBiases under Validating Webhook", func() { It("allows create when ManagedRedis is nil", func() { warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).To(BeEmpty()) }) + + It("rejects external Redis without host and port selectors", func() { + obj.Spec.Redis = map[string]appsv2.RedisSpec{ + appsv2.DefaultInstanceName: {ExternalRedis: &appsv2.RedisConnection{}}, + } + + _, err := validator.ValidateCreate(ctx, obj) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("externalRedis.host.name")) + Expect(err.Error()).To(ContainSubstring("externalRedis.host.key")) + Expect(err.Error()).To(ContainSubstring("externalRedis.port.name")) + Expect(err.Error()).To(ContainSubstring("externalRedis.port.key")) + }) + + It("allows external Redis with host and port selectors", func() { + obj.Spec.Redis = map[string]appsv2.RedisSpec{ + appsv2.DefaultInstanceName: { + ExternalRedis: &appsv2.RedisConnection{ + Host: secretKeySelector("redis", "host"), + Port: secretKeySelector("redis", "port"), + }, + }, + } + + warnings, err := validator.ValidateCreate(ctx, obj) + + Expect(err).NotTo(HaveOccurred()) + Expect(warnings).To(BeEmpty()) + }) + + It("allows external Redis while v1 literal values are pending materialization", func() { + obj.Annotations = map[string]string{ + appsv1.RedisPendingAnnotation: `{"host":"redis.example.com","port":"6379"}`, + } + obj.Spec.Redis = map[string]appsv2.RedisSpec{ + appsv2.DefaultInstanceName: {ExternalRedis: &appsv2.RedisConnection{}}, + } + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).NotTo(HaveOccurred()) Expect(warnings).To(BeEmpty()) }) @@ -293,7 +337,8 @@ var _ = Describe("WeightsAndBiases Webhook", func() { warnings, err := validator.ValidateCreate(ctx, obj) Expect(err).NotTo(HaveOccurred()) - Expect(warnings).To(BeEmpty()) }) + Expect(warnings).To(BeEmpty()) + }) It("rejects even Keeper replica counts", func() { obj.Spec.ClickHouse = map[string]appsv2.ClickHouseSpec{appsv2.DefaultInstanceName: {ManagedClickHouse: &appsv2.ManagedClickHouseSpec{ @@ -365,3 +410,10 @@ var _ = Describe("WeightsAndBiases Webhook", func() { func boolPtr(v bool) *bool { return &v } + +func secretKeySelector(name, key string) corev1.SecretKeySelector { + return corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: key, + } +} From 2a5f0a3d60180a3b2b783179d65ec544f38582ec Mon Sep 17 00:00:00 2001 From: wnevis-cmyk <wnevis@coreweave.com> Date: Mon, 27 Jul 2026 15:47:52 -0700 Subject: [PATCH 42/49] feat(backend): Consolidate object storage connections (#272) --- .../infra/external/objectstore/connection.go | 135 ------------- .../external/objectstore/connection_test.go | 108 ---------- .../infra/external/objectstore/objectstore.go | 77 ++++---- .../clickhouse/altinity/objectstorage.go | 125 +++--------- .../clickhouse/altinity/objectstorage_test.go | 34 ++-- .../infra/managed/clickhouse/altinity/spec.go | 38 +++- .../managed/clickhouse/altinity/spec_test.go | 28 +-- .../infra/managed/kafka/bufstream/config.go | 36 +--- .../managed/kafka/bufstream/config_test.go | 2 +- .../infra/managed/kafka/bufstream/spec.go | 20 +- .../managed/kafka/bufstream/spec_test.go | 10 +- .../infra/managed/kafka/bufstream/write.go | 68 +------ .../managed/objectstore/seaweedfs/conn.go | 99 ++++------ .../managed/objectstore/seaweedfs/write.go | 9 +- internal/controller/infra/objectstore/conn.go | 65 ++++++ .../controller/infra/objectstore/endpoint.go | 51 +++++ .../infra/objectstore/endpoint_test.go | 62 ++++++ .../{external => }/objectstore/pathstyle.go | 0 .../objectstore/pathstyle_test.go | 0 .../infra/objectstore/resolve_test.go | 163 +++++++++++++++ .../controller/infra/objectstore/secret.go | 185 ++++++++++++++++++ .../infra/objectstore/secret_test.go | 123 ++++++++++++ internal/controller/reconciler/clickhouse.go | 4 +- .../controller/reconciler/migrate_legacy.go | 4 +- 24 files changed, 871 insertions(+), 575 deletions(-) delete mode 100644 internal/controller/infra/external/objectstore/connection.go delete mode 100644 internal/controller/infra/external/objectstore/connection_test.go create mode 100644 internal/controller/infra/objectstore/conn.go create mode 100644 internal/controller/infra/objectstore/endpoint.go create mode 100644 internal/controller/infra/objectstore/endpoint_test.go rename internal/controller/infra/{external => }/objectstore/pathstyle.go (100%) rename internal/controller/infra/{external => }/objectstore/pathstyle_test.go (100%) create mode 100644 internal/controller/infra/objectstore/resolve_test.go create mode 100644 internal/controller/infra/objectstore/secret.go create mode 100644 internal/controller/infra/objectstore/secret_test.go diff --git a/internal/controller/infra/external/objectstore/connection.go b/internal/controller/infra/external/objectstore/connection.go deleted file mode 100644 index 4c8d4b9f..00000000 --- a/internal/controller/infra/external/objectstore/connection.go +++ /dev/null @@ -1,135 +0,0 @@ -package objectstore - -import ( - "fmt" - "net/url" - "strconv" - "strings" - - apiv2 "github.com/wandb/operator/api/v2" -) - -// ConnInfo is the resolved object-store connection: the read-side counterpart to WriteState, decoded back from the connection secret. -type ConnInfo struct { - Provider apiv2.ObjectStoreProvider - // URI is the provider-native location, e.g. "s3://bucket", "gs://bucket/prefix", or "https://acct.blob.core.windows.net/container". - URI string - // Bucket is the bare bucket/container name. - Bucket string - // Endpoint overrides the S3 API endpoint for S3-compatible providers (SeaweedFS, MinIO); empty for AWS S3, GCS, and Azure. - Endpoint string - Region string - AccessKey string - SecretKey string - // ForcePathStyle is required by most non-AWS S3-compatible providers. - ForcePathStyle bool - TlsEnabled bool - Port string - Path string -} - -// HasStaticCredentials reports whether explicit keys were provided; when false, credentials come from ambient identity (IAM role / workload identity). -func (c ConnInfo) HasStaticCredentials() bool { - return c.AccessKey != "" && c.SecretKey != "" -} - -// ParseConnection decodes an object-store connection secret's canonical `url` (scheme->provider, userinfo->creds, path->bucket, query->tls/region/forcePathStyle), falling back to discrete keys. -func ParseConnection(data map[string][]byte) (ConnInfo, error) { - get := func(k string) string { return string(data[k]) } - - raw := get("url") - if raw == "" { - return ConnInfo{}, fmt.Errorf("object store connection secret missing url") - } - u, err := url.Parse(raw) - if err != nil { - return ConnInfo{}, fmt.Errorf("parse object store url %q: %w", raw, err) - } - - info := ConnInfo{} - if u.User != nil { - info.AccessKey = u.User.Username() - if pw, ok := u.User.Password(); ok { - info.SecretKey = pw - } - } - if info.AccessKey == "" { - info.AccessKey = get("AccessKey") - } - if info.SecretKey == "" { - info.SecretKey = get("SecretKey") - } - - q := u.Query() - info.Region = q.Get("region") - if info.Region == "" { - info.Region = get("Region") - } - - switch strings.ToLower(u.Scheme) { - case "s3", "cw": - info.Provider = apiv2.ObjectStoreProviderS3 - bucket := strings.TrimPrefix(u.Path, "/") - host := u.Host - if bucket == "" { - // No path: bucket is the host (s3://my-bucket) or opaque part (s3:my-bucket), with no endpoint override. - if u.Opaque != "" { - bucket = u.Opaque - } else { - bucket = host - } - host = "" - } - info.Bucket = bucket - info.URI = "s3://" + bucket - // A host alongside a bucket path means an S3-compatible endpoint (SeaweedFS, MinIO); AWS S3 has no endpoint override. - if host != "" { - endpointScheme := "http" - if tls, _ := strconv.ParseBool(q.Get("tls")); tls { - endpointScheme = "https" - } - info.Endpoint = fmt.Sprintf("%s://%s", endpointScheme, host) - } - if fps := q.Get("forcePathStyle"); fps != "" { - info.ForcePathStyle, _ = strconv.ParseBool(fps) - } else { - // Non-AWS S3-compatible endpoints generally require path-style. - info.ForcePathStyle = info.Endpoint != "" - } - case "gs", "gcs": - info.Provider = apiv2.ObjectStoreProviderGCS - info.Bucket = u.Host - info.URI = "gs://" + u.Host + u.Path - case "azure", "az": - // az://<account>/<container>/<prefix> - info.Provider = apiv2.ObjectStoreProviderAzure - account := u.Host - container, prefix := splitBucketPath(u.Path) - info.Bucket = container - info.URI = azureBlobURI(account, container, prefix) - case "http", "https": - if !strings.Contains(u.Host, "blob.core.windows.net") { - return ConnInfo{}, fmt.Errorf("unsupported object store url scheme %q", u.Scheme) - } - info.Provider = apiv2.ObjectStoreProviderAzure - container, _ := splitBucketPath(u.Path) - info.Bucket = container - // Pass the container URI through verbatim (sans credentials/query). - info.URI = (&url.URL{Scheme: u.Scheme, Host: u.Host, Path: u.Path}).String() - default: - return ConnInfo{}, fmt.Errorf("unsupported object store url scheme %q", u.Scheme) - } - - if info.Bucket == "" && info.Provider != "" { - return ConnInfo{}, fmt.Errorf("object store url %q has no bucket/container", raw) - } - return info, nil -} - -func azureBlobURI(account, container, prefix string) string { - uri := fmt.Sprintf("https://%s.blob.core.windows.net/%s", account, container) - if prefix != "" { - uri += "/" + prefix - } - return uri -} diff --git a/internal/controller/infra/external/objectstore/connection_test.go b/internal/controller/infra/external/objectstore/connection_test.go deleted file mode 100644 index 4ef3b3bc..00000000 --- a/internal/controller/infra/external/objectstore/connection_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package objectstore - -import ( - "testing" - - "github.com/stretchr/testify/require" - - apiv2 "github.com/wandb/operator/api/v2" -) - -func TestParseConnectionS3Compatible(t *testing.T) { - // Managed SeaweedFS shape: s3://ak:sk@host:port/bucket?tls=false plus a - // discrete Region key. - data := map[string][]byte{ - "url": []byte("s3://ak:sk@seaweedfs.wandb.svc.cluster.local:8333/wandb-bucket?tls=false"), - "Region": []byte("us-east-1"), - } - - info, err := ParseConnection(data) - require.NoError(t, err) - require.Equal(t, apiv2.ObjectStoreProviderS3, info.Provider) - require.Equal(t, "wandb-bucket", info.Bucket) - require.Equal(t, "s3://wandb-bucket", info.URI) - require.Equal(t, "http://seaweedfs.wandb.svc.cluster.local:8333", info.Endpoint) - require.Equal(t, "us-east-1", info.Region) - require.Equal(t, "ak", info.AccessKey) - require.Equal(t, "sk", info.SecretKey) - require.True(t, info.ForcePathStyle) -} - -func TestParseConnectionS3TLS(t *testing.T) { - data := map[string][]byte{ - "url": []byte("s3://ak:sk@minio.example.com:9000/bucket?tls=true"), - } - info, err := ParseConnection(data) - require.NoError(t, err) - require.Equal(t, "https://minio.example.com:9000", info.Endpoint) -} - -func TestParseConnectionAWS(t *testing.T) { - // AWS S3 with IAM role: no host, no credentials. - data := map[string][]byte{ - "url": []byte("s3://my-bucket"), - "Region": []byte("us-west-2"), - } - info, err := ParseConnection(data) - require.NoError(t, err) - require.Equal(t, apiv2.ObjectStoreProviderS3, info.Provider) - require.Equal(t, "my-bucket", info.Bucket) - require.Empty(t, info.Endpoint) - require.False(t, info.ForcePathStyle) - require.False(t, info.HasStaticCredentials()) -} - -func TestParseConnectionDiscreteCredFallback(t *testing.T) { - data := map[string][]byte{ - "url": []byte("s3://host:9000/bucket"), - "AccessKey": []byte("ak"), - "SecretKey": []byte("sk"), - } - info, err := ParseConnection(data) - require.NoError(t, err) - require.Equal(t, "ak", info.AccessKey) - require.Equal(t, "sk", info.SecretKey) -} - -func TestParseConnectionGCS(t *testing.T) { - data := map[string][]byte{ - "url": []byte("gs://wandb-bucket/some/prefix"), - } - info, err := ParseConnection(data) - require.NoError(t, err) - require.Equal(t, apiv2.ObjectStoreProviderGCS, info.Provider) - require.Equal(t, "wandb-bucket", info.Bucket) - require.Equal(t, "gs://wandb-bucket/some/prefix", info.URI) -} - -func TestParseConnectionAzureHTTPS(t *testing.T) { - data := map[string][]byte{ - "url": []byte("https://acct.blob.core.windows.net/container/prefix"), - } - info, err := ParseConnection(data) - require.NoError(t, err) - require.Equal(t, apiv2.ObjectStoreProviderAzure, info.Provider) - require.Equal(t, "container", info.Bucket) - require.Equal(t, "https://acct.blob.core.windows.net/container/prefix", info.URI) -} - -func TestParseConnectionAzureScheme(t *testing.T) { - data := map[string][]byte{ - "url": []byte("az://acct:key@acct/container/prefix"), - } - info, err := ParseConnection(data) - require.NoError(t, err) - require.Equal(t, apiv2.ObjectStoreProviderAzure, info.Provider) - require.Equal(t, "container", info.Bucket) - require.Equal(t, "https://acct.blob.core.windows.net/container/prefix", info.URI) - require.Equal(t, "acct", info.AccessKey) - require.Equal(t, "key", info.SecretKey) -} - -func TestParseConnectionErrors(t *testing.T) { - _, err := ParseConnection(map[string][]byte{}) - require.Error(t, err) - - _, err = ParseConnection(map[string][]byte{"url": []byte("ftp://nope/bucket")}) - require.Error(t, err) -} diff --git a/internal/controller/infra/external/objectstore/objectstore.go b/internal/controller/infra/external/objectstore/objectstore.go index 1084957d..60928521 100644 --- a/internal/controller/infra/external/objectstore/objectstore.go +++ b/internal/controller/infra/external/objectstore/objectstore.go @@ -9,16 +9,18 @@ import ( apiv2 "github.com/wandb/operator/api/v2" "github.com/wandb/operator/internal/controller/infra/external" + osconn "github.com/wandb/operator/internal/controller/infra/objectstore" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) const ConnectionSecretName = "wandb-objectstore-connection" +// connectionSecretName builds the connection secret name for an object-store +// instance key, using the shared default name for the primary instance. func connectionSecretName(key string) string { if key == "" || key == apiv2.DefaultInstanceName { return ConnectionSecretName @@ -26,6 +28,9 @@ func connectionSecretName(key string) string { return fmt.Sprintf("%s-%s", ConnectionSecretName, key) } +// WriteState resolves the external object-store fields into a connection secret +// and returns the reconcile conditions plus the resulting ObjectStoreConnection +// selectors (nil on error). func WriteState( ctx context.Context, c client.Client, @@ -69,45 +74,46 @@ func WriteState( if provider == "" { provider = apiv2.ObjectStoreProviderS3 } - data["Provider"] = string(provider) + + connInfo := osconn.ConnInfo{ + Provider: provider, + Endpoint: data["Host"], + Port: data["Port"], + AccessKey: data["AccessKey"], + SecretKey: data["SecretKey"], + Bucket: data["Bucket"], + Path: data["Path"], + Region: data["Region"], + } + if tls, err := strconv.ParseBool(data["TlsEnabled"]); err == nil { + connInfo.TlsEnabled = tls + } switch provider { case apiv2.ObjectStoreProviderGCS: - data["url"] = buildGCSURL(data) + connInfo.URL = buildGCSURL(data) case apiv2.ObjectStoreProviderAzure: - data["url"] = buildAzureURL(data) + connInfo.URL = buildAzureURL(data) default: // Consumers (Bufstream) render this verbatim, so derive it when the CR doesn't say. - if _, ok := data["ForcePathStyle"]; !ok { - data["ForcePathStyle"] = strconv.FormatBool(RequiresPathStyle(data["Host"])) + if fps, ok := data["ForcePathStyle"]; ok { + connInfo.ForcePathStyle, _ = strconv.ParseBool(fps) + } else { + connInfo.ForcePathStyle = osconn.RequiresPathStyle(data["Host"]) } - data["url"] = buildS3URL(data) + connInfo.URL = buildS3URL(data) } nsName := types.NamespacedName{Namespace: wandb.Namespace, Name: connectionSecretName(key)} - if conditions := external.WriteConnectionSecret(ctx, c, wandb, nsName, data); conditions != nil { + if conditions := external.WriteConnectionSecret(ctx, c, wandb, nsName, connInfo.ToSecretData()); conditions != nil { return conditions, nil } - localRef := corev1.LocalObjectReference{Name: nsName.Name} - // ResolveFields only writes non-empty values, so any field that is - // legitimately absent for some deployment must be optional: Host (plain - // AWS S3 with no custom endpoint), AccessKey/SecretKey (IAM-role / - // workload-identity auth), Region (MinIO or region supplied out-of-band). - // url and Bucket are always written. - return nil, &apiv2.ObjectStoreConnection{ - Provider: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Provider", Optional: ptr.To(false)}, - URL: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "url", Optional: ptr.To(false)}, - Endpoint: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Host", Optional: ptr.To(true)}, - Port: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Port", Optional: ptr.To(true)}, - AccessKey: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "AccessKey", Optional: ptr.To(true)}, - SecretKey: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "SecretKey", Optional: ptr.To(true)}, - Bucket: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Bucket", Optional: ptr.To(false)}, - Path: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Path", Optional: ptr.To(true)}, - Region: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Region", Optional: ptr.To(true)}, - TlsEnabled: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "TlsEnabled", Optional: ptr.To(true)}, - ForcePathStyle: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "ForcePathStyle", Optional: ptr.To(true)}, - } + // ToSecretData only writes non-empty values, so any field that is + // legitimately absent for some deployment (Host for plain AWS S3, + // AccessKey/SecretKey for IAM-role / workload-identity auth, Region for + // MinIO) stays optional; url/Provider/Bucket are always required. + return nil, connInfo.ToObjectStoreConnection(nsName.Name, false) } // buildS3URL assembles s3://[accessKey:secretKey@][host[:port]]/bucket[/path]; host and creds are omitted for native AWS S3 / IAM-role auth. @@ -131,7 +137,7 @@ func buildS3URL(data map[string]string) string { // buildGCSURL assembles gs://<bucket>[/path]; creds default to workload identity, or accessKey (SA email) + secretKey (PEM key) as userinfo. func buildGCSURL(data map[string]string) string { - bucket, path := splitBucketPath(data["Bucket"]) + bucket, path := osconn.SplitBucketPath(data["Bucket"]) path = joinBucketPrefix(path, data["Path"]) bucketURL := url.URL{Scheme: "gs", Host: bucket} if path != "" { @@ -146,7 +152,7 @@ func buildGCSURL(data map[string]string) string { // buildAzureURL assembles az://<account>/<container>[/path] from accessKey (account), bucket (container), and secretKey (account key, when set). func buildAzureURL(data map[string]string) string { account := data["AccessKey"] - container, path := splitBucketPath(data["Bucket"]) + container, path := osconn.SplitBucketPath(data["Bucket"]) path = joinBucketPrefix(path, data["Path"]) bucketURL := url.URL{Scheme: "az", Host: account, Path: "/" + container} if path != "" { @@ -171,15 +177,8 @@ func joinBucketPrefix(base, prefix string) string { } } -// splitBucketPath splits "bucket/optional/prefix" into the leading bucket (or container) segment and the remaining object prefix. -func splitBucketPath(raw string) (bucket, path string) { - trimmed := strings.TrimPrefix(raw, "/") - if slash := strings.IndexByte(trimmed, '/'); slash >= 0 { - return trimmed[:slash], trimmed[slash+1:] - } - return trimmed, "" -} - +// ReadState is a no-op for external object stores; it passes through the +// conditions produced by WriteState since there is no additional state to read. func ReadState( _ context.Context, _ client.Client, @@ -190,6 +189,8 @@ func ReadState( return newConditions } +// DeleteConnectionSecret removes the connection secret written for the given +// object-store instance key. func DeleteConnectionSecret(ctx context.Context, c client.Client, wandb *apiv2.WeightsAndBiases, key string) error { return external.DeleteConnectionSecret(ctx, c, types.NamespacedName{ Namespace: wandb.Namespace, diff --git a/internal/controller/infra/managed/clickhouse/altinity/objectstorage.go b/internal/controller/infra/managed/clickhouse/altinity/objectstorage.go index 3b957677..ba5dea7c 100644 --- a/internal/controller/infra/managed/clickhouse/altinity/objectstorage.go +++ b/internal/controller/infra/managed/clickhouse/altinity/objectstorage.go @@ -7,7 +7,7 @@ import ( "strings" apiv2 "github.com/wandb/operator/api/v2" - "github.com/wandb/operator/pkg/utils" + "github.com/wandb/operator/internal/controller/infra/objectstore" "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" chtypes "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/common/types" corev1 "k8s.io/api/core/v1" @@ -33,91 +33,34 @@ const ( storageConfigKey = "storage_configuration" ) -// ObjectStorageConn holds the resolved bucket connection used to configure the -// S3-backed disk: endpoint/region as literals, credentials as secret references. -type ObjectStorageConn struct { - // Endpoint is the full http(s) URL incl. bucket + prefix, trailing slash. - Endpoint string - Region string - // UseEnvCredentials uses ambient creds (IAM role) instead of access keys. - UseEnvCredentials bool - AccessKeyRef corev1.SecretKeySelector - SecretKeyRef corev1.SecretKeySelector -} - -// ResolveObjectStorage reads the object-store connection's secret key references -// (which may span multiple secrets) and derives the S3 disk details. +// ResolveObjectStorage resolves the connection and builds the S3 endpoint. func ResolveObjectStorage( ctx context.Context, cl client.Client, spec *apiv2.ManagedClickHouseSpec, conn *apiv2.ObjectStoreConnection, -) (*ObjectStorageConn, error) { +) (*objectstore.ConnInfo, string, error) { if spec == nil { - return nil, nil - } - if conn == nil { - return nil, fmt.Errorf("object store connection is not available yet") + return nil, "", nil } - r := &utils.ConnSecretResolver{Client: cl, Namespace: spec.Namespace, Cache: map[string]*corev1.Secret{}} - - bucket, err := r.Value(ctx, conn.Bucket) + ci, err := objectstore.Resolve(ctx, cl, spec.Namespace, conn) if err != nil { - return nil, err - } - if bucket == "" { - return nil, fmt.Errorf("object store connection has no bucket reference") + return nil, "", err } - - host, err := r.Value(ctx, conn.Endpoint) - if err != nil { - return nil, err - } - port, err := r.Value(ctx, conn.Port) - if err != nil { - return nil, err - } - region, err := r.Value(ctx, conn.Region) - if err != nil { - return nil, err - } - accessKey, err := r.Value(ctx, conn.AccessKey) - if err != nil { - return nil, err - } - - tlsEnabledString, err := r.Value(ctx, conn.TlsEnabled) - if err != nil { - return nil, err - } - - tlsEnabled, err := strconv.ParseBool(tlsEnabledString) - if err != nil { - tlsEnabled = false - } - - // The connection has no scheme reference; honor one advertised in the - // endpoint value, otherwise let buildEndpoint infer from spec.Insecure. - scheme := "" - if i := strings.Index(host, "://"); i >= 0 { - scheme, host = host[:i], host[i+len("://"):] + if ci.Bucket == "" { + return nil, "", fmt.Errorf("object store connection has no bucket reference") } - endpoint, err := buildEndpoint(scheme, host, port, bucket, region, objectStoragePrefix(spec), tlsEnabled) + endpoint, err := buildEndpoint(ci, objectStoragePrefix(spec)) if err != nil { - return nil, err + return nil, "", err } - return &ObjectStorageConn{ - Endpoint: endpoint, - Region: region, - UseEnvCredentials: accessKey == "", - AccessKeyRef: conn.AccessKey, - SecretKeyRef: conn.SecretKey, - }, nil + return &ci, endpoint, nil } +// objectStoragePrefix returns the normalized in-bucket prefix for the spec. func objectStoragePrefix(spec *apiv2.ManagedClickHouseSpec) string { return normalizePrefix(spec.ObjectStorage.Prefix) } @@ -132,46 +75,34 @@ func normalizePrefix(prefix string) string { return prefix + "/" } -// buildEndpoint builds the S3 disk endpoint: path-style for a custom host, else -// the AWS virtual-hosted URL derived from the region. -func buildEndpoint(scheme, host, port, bucket, region, prefix string, secure bool) (string, error) { - if host != "" { - if scheme == "" { - scheme = "http" - if secure { - scheme = "https" - } - } - hostport := host - if port != "" { - hostport = host + ":" + port - } - return fmt.Sprintf("%s://%s/%s/%s", scheme, hostport, bucket, prefix), nil +// buildEndpoint builds the S3 disk endpoint: path-style for a custom endpoint, +// else the AWS virtual-hosted URL derived from the region. +func buildEndpoint(ci objectstore.ConnInfo, prefix string) (string, error) { + if base := ci.EndpointURL(); base != "" { + return fmt.Sprintf("%s/%s/%s", base, ci.Bucket, prefix), nil } - if region == "" { + if ci.Region == "" { return "", fmt.Errorf("object store has no Host and no Region; cannot derive an S3 endpoint") } - return fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", bucket, region, prefix), nil + return fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", ci.Bucket, ci.Region, prefix), nil } -// applyStorageConfiguration adds the <storage_configuration> (S3 disk + cache + -// policy) to settings. Credentials are secret references so the Altinity -// operator injects them via from_env rather than as plaintext. -// TODO(dpanzella): Currently only supports S3 compatible storage, add support for Azure and GCS -func applyStorageConfiguration(settings *v1.Settings, oc *ObjectStorageConn, cacheMaxSizeBytes int64) { +// applyStorageConfiguration sets the S3 disk, cache, and storage policy. +// TODO(dpanzella): only S3 supported; add Azure and GCS. +func applyStorageConfiguration(settings *v1.Settings, ci *objectstore.ConnInfo, endpoint string, cacheMaxSizeBytes int64) { disk := diskKey(s3DiskName) settings.Set(disk("type"), v1.NewSettingScalar("s3")) - settings.Set(disk("endpoint"), v1.NewSettingScalar(oc.Endpoint)) + settings.Set(disk("endpoint"), v1.NewSettingScalar(endpoint)) settings.Set(disk("metadata_path"), v1.NewSettingScalar(s3MetadataPath)) - if oc.Region != "" { - settings.Set(disk("region"), v1.NewSettingScalar(oc.Region)) + if ci.Region != "" { + settings.Set(disk("region"), v1.NewSettingScalar(ci.Region)) } - if oc.UseEnvCredentials { + if ci.AccessKey == "" { settings.Set(disk("use_environment_credentials"), v1.NewSettingScalar("true")) } else { - settings.Set(disk("access_key_id"), secretSetting(oc.AccessKeyRef)) - settings.Set(disk("secret_access_key"), secretSetting(oc.SecretKeyRef)) + settings.Set(disk("access_key_id"), secretSetting(ci.AccessKeyRef)) + settings.Set(disk("secret_access_key"), secretSetting(ci.SecretKeyRef)) } cache := diskKey(s3CacheDiskName) diff --git a/internal/controller/infra/managed/clickhouse/altinity/objectstorage_test.go b/internal/controller/infra/managed/clickhouse/altinity/objectstorage_test.go index 982ce3f4..4b1c468c 100644 --- a/internal/controller/infra/managed/clickhouse/altinity/objectstorage_test.go +++ b/internal/controller/infra/managed/clickhouse/altinity/objectstorage_test.go @@ -5,37 +5,46 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/wandb/operator/internal/controller/infra/objectstore" "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" corev1 "k8s.io/api/core/v1" ) var _ = Describe("Object storage endpoint", func() { It("uses the scheme reported by the connection for a custom host", func() { - ep, err := buildEndpoint("http", "seaweedfs.wandb.svc.cluster.local", "80", "bucket", "us-east-1", "clickhouse/", false) + ep, err := buildEndpoint(objectstore.ConnInfo{ + Endpoint: "http://seaweedfs.wandb.svc.cluster.local", Port: "80", Bucket: "bucket", Region: "us-east-1", + }, "clickhouse/") Expect(err).NotTo(HaveOccurred()) Expect(ep).To(Equal("http://seaweedfs.wandb.svc.cluster.local:80/bucket/clickhouse/")) }) It("defaults to https for an external host with tls enabled", func() { - ep, err := buildEndpoint("", "minio.example.com", "9000", "data", "", "clickhouse/", true) + ep, err := buildEndpoint(objectstore.ConnInfo{ + Endpoint: "minio.example.com", Port: "9000", Bucket: "data", TlsEnabled: true, + }, "clickhouse/") Expect(err).NotTo(HaveOccurred()) Expect(ep).To(Equal("https://minio.example.com:9000/data/clickhouse/")) }) It("uses http for an external host when tls is disabled", func() { - ep, err := buildEndpoint("", "minio.example.com", "9000", "data", "", "clickhouse/", false) + ep, err := buildEndpoint(objectstore.ConnInfo{ + Endpoint: "minio.example.com", Port: "9000", Bucket: "data", + }, "clickhouse/") Expect(err).NotTo(HaveOccurred()) Expect(ep).To(Equal("http://minio.example.com:9000/data/clickhouse/")) }) It("derives an AWS virtual-hosted endpoint when no host is set", func() { - ep, err := buildEndpoint("", "", "", "my-bucket", "us-west-2", "clickhouse/", true) + ep, err := buildEndpoint(objectstore.ConnInfo{ + Bucket: "my-bucket", Region: "us-west-2", TlsEnabled: true, + }, "clickhouse/") Expect(err).NotTo(HaveOccurred()) Expect(ep).To(Equal("https://my-bucket.s3.us-west-2.amazonaws.com/clickhouse/")) }) It("errors when neither host nor region is available", func() { - _, err := buildEndpoint("", "", "", "my-bucket", "", "clickhouse/", true) + _, err := buildEndpoint(objectstore.ConnInfo{Bucket: "my-bucket"}, "clickhouse/") Expect(err).To(HaveOccurred()) }) }) @@ -54,14 +63,15 @@ var _ = Describe("Object storage prefix", func() { var _ = Describe("Storage configuration settings", func() { It("defines an s3 disk, cache, policy, default routing, and local system logs", func() { ref := corev1.LocalObjectReference{Name: "objstore-conn"} - oc := &ObjectStorageConn{ - Endpoint: "http://host:80/bucket/clickhouse/", + ci := &objectstore.ConnInfo{ Region: "us-east-1", + AccessKey: "AKIA", + SecretKey: "secret", AccessKeyRef: corev1.SecretKeySelector{LocalObjectReference: ref, Key: "AccessKey"}, SecretKeyRef: corev1.SecretKeySelector{LocalObjectReference: ref, Key: "SecretKey"}, } settings := v1.NewSettings() - applyStorageConfiguration(settings, oc, 8<<30) + applyStorageConfiguration(settings, ci, "http://host:80/bucket/clickhouse/", 8<<30) Expect(settings.Get("storage_configuration/disks/s3_disk/type").String()).To(Equal("s3")) Expect(settings.Get("storage_configuration/disks/s3_disk/endpoint").String()).To(Equal("http://host:80/bucket/clickhouse/")) @@ -83,9 +93,9 @@ var _ = Describe("Storage configuration settings", func() { }) It("uses ambient credentials when no access keys are present", func() { - oc := &ObjectStorageConn{Endpoint: "https://b.s3.us-east-1.amazonaws.com/clickhouse/", UseEnvCredentials: true} + ci := &objectstore.ConnInfo{} settings := v1.NewSettings() - applyStorageConfiguration(settings, oc, 1024) + applyStorageConfiguration(settings, ci, "https://b.s3.us-east-1.amazonaws.com/clickhouse/", 1024) Expect(settings.Get("storage_configuration/disks/s3_disk/use_environment_credentials").String()).To(Equal("true")) Expect(settings.Has("storage_configuration/disks/s3_disk/access_key_id")).To(BeFalse()) @@ -93,9 +103,9 @@ var _ = Describe("Storage configuration settings", func() { }) It("renders the s3 disk before the cache disk that wraps it", func() { - oc := &ObjectStorageConn{Endpoint: "http://host:80/bucket/clickhouse/", UseEnvCredentials: true} + ci := &objectstore.ConnInfo{} settings := v1.NewSettings() - applyStorageConfiguration(settings, oc, 1<<30) + applyStorageConfiguration(settings, ci, "http://host:80/bucket/clickhouse/", 1<<30) // ClickHouse initializes disks in document order and requires the wrapped // disk to be defined before the cache disk; verify the rendered XML order. diff --git a/internal/controller/infra/managed/clickhouse/altinity/spec.go b/internal/controller/infra/managed/clickhouse/altinity/spec.go index 2aa9bf6e..1198af58 100644 --- a/internal/controller/infra/managed/clickhouse/altinity/spec.go +++ b/internal/controller/infra/managed/clickhouse/altinity/spec.go @@ -7,6 +7,7 @@ import ( apiv2 "github.com/wandb/operator/api/v2" "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/objectstore" "github.com/wandb/operator/internal/controller/infra/managed/clickhouse/altinity/keeper" "github.com/wandb/operator/internal/logx" "github.com/wandb/operator/pkg/utils" @@ -33,6 +34,8 @@ const ( objectStoreWaitDelaySeconds = 2 ) +// ClickHouseImage resolves the ClickHouse server image from the manifest, +// falling back to the hardcoded default for older manifests that omit it. func ClickHouseImage(img manifest.ImageRef, globalImageRegistry string) string { if out := img.GetImage(globalImageRegistry); out != "" { return out @@ -56,6 +59,8 @@ const ( clickHouseCapabilityAll corev1.Capability = "ALL" ) +// clickHousePodSecurityContext returns the pod security context, omitting the +// fixed UID/GID/FSGroup on OpenShift where the platform assigns them. func clickHousePodSecurityContext() *corev1.PodSecurityContext { if utils.IsOpenShift() { return &corev1.PodSecurityContext{ @@ -73,6 +78,8 @@ func clickHousePodSecurityContext() *corev1.PodSecurityContext { } } +// clickHouseContainerSecurityContext returns the container security context, +// pinning the fixed UID/GID off OpenShift and always dropping all capabilities. func clickHouseContainerSecurityContext() *corev1.SecurityContext { securityContext := &corev1.SecurityContext{ RunAsNonRoot: ptr.To(true), @@ -89,6 +96,8 @@ func clickHouseContainerSecurityContext() *corev1.SecurityContext { return securityContext } +// clickHouseWritableVolumes returns the emptyDir volumes that back the writable +// paths a read-only-root-filesystem ClickHouse container still needs. func clickHouseWritableVolumes() []corev1.Volume { return []corev1.Volume{ writableEmptyDirVolume(clickHouseTmpVolumeName), @@ -97,6 +106,8 @@ func clickHouseWritableVolumes() []corev1.Volume { } } +// clickHouseWritableVolumeMounts returns the mounts pairing the writable +// emptyDir volumes with their in-container paths. func clickHouseWritableVolumeMounts() []corev1.VolumeMount { return []corev1.VolumeMount{ {Name: clickHouseTmpVolumeName, MountPath: clickHouseTmpMountPath}, @@ -105,10 +116,12 @@ func clickHouseWritableVolumeMounts() []corev1.VolumeMount { } } +// clickHouseRuntimeDefaultSeccompProfile returns the RuntimeDefault seccomp profile. func clickHouseRuntimeDefaultSeccompProfile() *corev1.SeccompProfile { return &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault} } +// writableEmptyDirVolume returns a named emptyDir volume. func writableEmptyDirVolume(name string) corev1.Volume { return corev1.Volume{ Name: name, @@ -118,10 +131,13 @@ func writableEmptyDirVolume(name string) corev1.Volume { } } +// ToServiceAccount builds the ClickHouse ServiceAccount, automounting its token +// only when object-store credentials are ambient (IAM / workload identity). +// Returns nil when the spec opts out of ServiceAccount creation. func ToServiceAccount( wandb *apiv2.WeightsAndBiases, spec *apiv2.ManagedClickHouseSpec, - objStorage *ObjectStorageConn, + objStorage *objectstore.ConnInfo, scheme *runtime.Scheme, ) (*corev1.ServiceAccount, error) { if spec.ServiceAccount.Create != nil && !*spec.ServiceAccount.Create { @@ -135,7 +151,9 @@ func ToServiceAccount( Labels: BuildWandbClickhouseLabels(wandb), Annotations: spec.ServiceAccount.Annotations, }, - AutomountServiceAccountToken: ptr.To(objStorage.UseEnvCredentials), + // Ambient (IAM/workload-identity) credentials require the projected SA + // token; static access keys don't, so only automount when creds are ambient. + AutomountServiceAccountToken: ptr.To(!objStorage.HasStaticCredentials()), } if wandb.Namespace == spec.Namespace { if err := ctrl.SetControllerReference(wandb, serviceAccount, scheme); err != nil { @@ -145,6 +163,8 @@ func ToServiceAccount( return serviceAccount, nil } +// clickHouseServiceAccountName returns the configured ServiceAccount name, +// defaulting to the spec name when unset. func clickHouseServiceAccountName(spec *apiv2.ManagedClickHouseSpec) string { if spec.ServiceAccount.ServiceAccountName != "" { return spec.ServiceAccount.ServiceAccountName @@ -160,7 +180,8 @@ func ToClickHouseVendorSpec( wandb *apiv2.WeightsAndBiases, spec *apiv2.ManagedClickHouseSpec, scheme *runtime.Scheme, - objStorage *ObjectStorageConn, + objStorage *objectstore.ConnInfo, + objStorageEndpoint string, waitForObjectStore bool, mfst manifest.Manifest, ) (*v1.ClickHouseInstallation, error) { @@ -206,7 +227,7 @@ func ToClickHouseVendorSpec( serverSettings := v1.NewSettings() // Define the S3 disk + cache + storage policy and make it the server-wide default. - applyStorageConfiguration(serverSettings, objStorage, cacheMaxSizeBytes) + applyStorageConfiguration(serverSettings, objStorage, objStorageEndpoint, cacheMaxSizeBytes) // Enable built-in Prometheus metrics endpoint if telemetry is enabled if spec.Telemetry.Enabled { @@ -226,7 +247,7 @@ func ToClickHouseVendorSpec( clickHouseImage := ClickHouseImage(mfst.Clickhouse["default"].Images["server"], wandb.Spec.Global.ImageRegistry) podSpec := corev1.PodSpec{ ServiceAccountName: clickHouseServiceAccountName(spec), - AutomountServiceAccountToken: ptr.To(objStorage.UseEnvCredentials), + AutomountServiceAccountToken: ptr.To(!objStorage.HasStaticCredentials()), SecurityContext: clickHousePodSecurityContext(), Affinity: wandb.GetAffinity(spec.ManagedInfraSpec), Tolerations: *wandb.GetTolerations(spec.ManagedInfraSpec), @@ -241,7 +262,7 @@ func ToClickHouseVendorSpec( }, } if waitForObjectStore { - podSpec.InitContainers = []corev1.Container{clickHouseObjectStoreWaitContainer(objStorage.Endpoint, clickHouseImage)} + podSpec.InitContainers = []corev1.Container{clickHouseObjectStoreWaitContainer(objStorageEndpoint, clickHouseImage)} } if len(spec.Config.Resources.Requests) > 0 || len(spec.Config.Resources.Limits) > 0 { @@ -331,6 +352,9 @@ func ToClickHouseVendorSpec( return chi, nil } +// clickHouseObjectStoreWaitContainer returns an init container that blocks until +// the object-store endpoint is reachable, so ClickHouse does not start before +// its backing bucket is available. func clickHouseObjectStoreWaitContainer(endpoint, image string) corev1.Container { // The existing ClickHouse image includes wget. Any HTTP response below 500 // proves DNS and the S3 API are reachable; authentication remains ClickHouse's @@ -361,10 +385,12 @@ exit 1`, } } +// BuildWandbClickhouseLabels returns the standard W&B labels for the ClickHouse module. func BuildWandbClickhouseLabels(wandb *apiv2.WeightsAndBiases) map[string]string { return common.BuildWandbLabels(wandb, ClickhouseModuleName) } +// ToClickHouseOnDeleteRule builds the on-delete retention rule for the ClickHouse module. func ToClickHouseOnDeleteRule(wandb *apiv2.WeightsAndBiases, retentionPolicy apiv2.RetentionPolicy) common.OnDeleteRule { return common.ToOnDeleteRule(wandb, retentionPolicy, ClickhouseModuleName) } diff --git a/internal/controller/infra/managed/clickhouse/altinity/spec_test.go b/internal/controller/infra/managed/clickhouse/altinity/spec_test.go index fe219f70..b0d4bd46 100644 --- a/internal/controller/infra/managed/clickhouse/altinity/spec_test.go +++ b/internal/controller/infra/managed/clickhouse/altinity/spec_test.go @@ -6,6 +6,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/infra/objectstore" "github.com/wandb/operator/internal/controller/infra/managed/clickhouse/altinity/keeper" "github.com/wandb/operator/pkg/utils" chiv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" @@ -24,7 +25,7 @@ var _ = Describe("ClickHouse vendor specs", func() { It("renders hardened pod templates with writable runtime mounts", func() { wandb := clickHouseWandb() - chi, err := ToClickHouseVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, clickHouseScheme(), testObjectStorageConn(), true, manifest.Manifest{}) + chi, err := ToClickHouseVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, clickHouseScheme(), testObjectStorageConn(), testObjectStorageEndpoint, true, manifest.Manifest{}) Expect(err).NotTo(HaveOccurred()) Expect(chi).NotTo(BeNil()) Expect(chi.Spec.Templates.PodTemplates).To(HaveLen(1)) @@ -51,7 +52,7 @@ var _ = Describe("ClickHouse vendor specs", func() { Expect(wait.Args[0]).NotTo(ContainSubstring("head-bucket")) Expect(wait.Args[0]).NotTo(ContainSubstring("AccessKey")) Expect(wait.Args[0]).NotTo(ContainSubstring("SecretKey")) - Expect(wait.Args[2]).To(Equal(testObjectStorageConn().Endpoint)) + Expect(wait.Args[2]).To(Equal(testObjectStorageEndpoint)) Expect(wait.Env).To(BeEmpty()) container := podSpec.Containers[0] Expect(container.Image).To(Equal(ClickHouseImage(manifest.ImageRef{}, ""))) @@ -66,7 +67,7 @@ var _ = Describe("ClickHouse vendor specs", func() { utils.SetOpenShiftMode(true) wandb := clickHouseWandb() - chi, err := ToClickHouseVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, clickHouseScheme(), testObjectStorageConn(), true, manifest.Manifest{}) + chi, err := ToClickHouseVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, clickHouseScheme(), testObjectStorageConn(), testObjectStorageEndpoint, true, manifest.Manifest{}) Expect(err).NotTo(HaveOccurred()) Expect(chi).NotTo(BeNil()) @@ -77,7 +78,7 @@ var _ = Describe("ClickHouse vendor specs", func() { It("backs storage with the object store, sets a default policy, and wires keeper", func() { wandb := clickHouseWandb() - chi, err := ToClickHouseVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, clickHouseScheme(), testObjectStorageConn(), true, manifest.Manifest{}) + chi, err := ToClickHouseVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, clickHouseScheme(), testObjectStorageConn(), testObjectStorageEndpoint, true, manifest.Manifest{}) Expect(err).NotTo(HaveOccurred()) Expect(chi).NotTo(BeNil()) @@ -110,7 +111,7 @@ var _ = Describe("ClickHouse vendor specs", func() { It("does not gate ClickHouse for bring-your-own object storage", func() { wandb := clickHouseWandb() - chi, err := ToClickHouseVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, clickHouseScheme(), testObjectStorageConn(), false, manifest.Manifest{}) + chi, err := ToClickHouseVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, clickHouseScheme(), testObjectStorageConn(), testObjectStorageEndpoint, false, manifest.Manifest{}) Expect(err).NotTo(HaveOccurred()) Expect(chi.Spec.Templates.PodTemplates[0].Spec.InitContainers).To(BeEmpty()) }) @@ -125,7 +126,9 @@ var _ = Describe("ClickHouse vendor specs", func() { }, } objStorage := testObjectStorageConn() - objStorage.UseEnvCredentials = true + // Ambient credentials: no static access/secret keys are set. + objStorage.AccessKey = "" + objStorage.SecretKey = "" serviceAccount, err := ToServiceAccount(wandb, spec, objStorage, clickHouseScheme()) Expect(err).NotTo(HaveOccurred()) @@ -134,7 +137,7 @@ var _ = Describe("ClickHouse vendor specs", func() { Expect(serviceAccount.AutomountServiceAccountToken).NotTo(BeNil()) Expect(*serviceAccount.AutomountServiceAccountToken).To(BeTrue()) - chi, err := ToClickHouseVendorSpec(context.Background(), wandb, spec, clickHouseScheme(), objStorage, false, manifest.Manifest{}) + chi, err := ToClickHouseVendorSpec(context.Background(), wandb, spec, clickHouseScheme(), objStorage, testObjectStorageEndpoint, false, manifest.Manifest{}) Expect(err).NotTo(HaveOccurred()) podSpec := chi.Spec.Templates.PodTemplates[0].Spec Expect(podSpec.ServiceAccountName).To(Equal(serviceAccount.Name)) @@ -155,17 +158,20 @@ var _ = Describe("ClickHouse vendor specs", func() { Expect(err).NotTo(HaveOccurred()) Expect(serviceAccount).To(BeNil()) - chi, err := ToClickHouseVendorSpec(context.Background(), wandb, spec, clickHouseScheme(), testObjectStorageConn(), false, manifest.Manifest{}) + chi, err := ToClickHouseVendorSpec(context.Background(), wandb, spec, clickHouseScheme(), testObjectStorageConn(), testObjectStorageEndpoint, false, manifest.Manifest{}) Expect(err).NotTo(HaveOccurred()) Expect(chi.Spec.Templates.PodTemplates[0].Spec.ServiceAccountName).To(Equal("existing-clickhouse-identity")) }) }) -func testObjectStorageConn() *ObjectStorageConn { +const testObjectStorageEndpoint = "http://seaweedfs.wandb.svc.cluster.local:80/bucket/clickhouse/" + +func testObjectStorageConn() *objectstore.ConnInfo { ref := corev1.LocalObjectReference{Name: "objstore-conn"} - return &ObjectStorageConn{ - Endpoint: "http://seaweedfs.wandb.svc.cluster.local:80/bucket/clickhouse/", + return &objectstore.ConnInfo{ Region: "us-east-1", + AccessKey: "AKIA", + SecretKey: "secret", AccessKeyRef: corev1.SecretKeySelector{LocalObjectReference: ref, Key: "AccessKey"}, SecretKeyRef: corev1.SecretKeySelector{LocalObjectReference: ref, Key: "SecretKey"}, } diff --git a/internal/controller/infra/managed/kafka/bufstream/config.go b/internal/controller/infra/managed/kafka/bufstream/config.go index b4c30f4e..7f232a46 100644 --- a/internal/controller/infra/managed/kafka/bufstream/config.go +++ b/internal/controller/infra/managed/kafka/bufstream/config.go @@ -2,12 +2,11 @@ package bufstream import ( "fmt" - "strings" "gopkg.in/yaml.v3" apiv2 "github.com/wandb/operator/api/v2" - "github.com/wandb/operator/internal/controller/infra/external/objectstore" + "github.com/wandb/operator/internal/controller/infra/objectstore" ) type dataSource struct { @@ -74,16 +73,7 @@ func renderBufstreamConfig(clusterName, advertiseHost string, etcdAddresses []st // Isolate Bufstream's objects under a dedicated key prefix (the cluster name) // so they never collide with W&B artifact data, which shares the same bucket. // storage is passed by value, so this only affects the rendered config. - uri := "" - switch storage.Provider { - case apiv2.ObjectStoreProviderS3: - uri = fmt.Sprintf("s3://%s", storage.Bucket) - case apiv2.ObjectStoreProviderGCS: - uri = fmt.Sprintf("gs://%s", storage.Bucket) - case apiv2.ObjectStoreProviderAzure: - // For Azure the connection's AccessKey carries the storage account. - uri = fmt.Sprintf("https://%s.blob.core.windows.net/%s", storage.AccessKey, storage.Bucket) - } + uri := storage.ProviderURI() if storage.Path != "" { uri = fmt.Sprintf("%s/%s", uri, storage.Path) @@ -139,29 +129,19 @@ func renderData(storage objectstore.ConnInfo, uri string) (bufstreamData, error) } } +// renderS3Storage maps the resolved connection onto Bufstream's S3 data config, +// applying the region default, endpoint, and path-style, and wiring env-var +// credential sources only when static keys are present. func renderS3Storage(storage objectstore.ConnInfo, uri string) *bufstreamS3 { region := storage.Region if region == "" { - region = "us-east-1" + region = objectstore.DefaultRegion } - endpoint := "" - if storage.Endpoint != "" { - if strings.Contains(storage.Endpoint, "://") { - endpoint = storage.Endpoint - } else { - scheme := "http" - if storage.TlsEnabled { - scheme = "https" - } - endpoint = fmt.Sprintf("%s://%s:%s", scheme, storage.Endpoint, storage.Port) - - } - } s3 := &bufstreamS3{ URI: uri, Region: region, - Endpoint: endpoint, + Endpoint: storage.EndpointURL(), ForcePathStyle: storage.ForcePathStyle, } if storage.HasStaticCredentials() { @@ -171,6 +151,8 @@ func renderS3Storage(storage objectstore.ConnInfo, uri string) *bufstreamS3 { return s3 } +// renderAzureStorage maps the resolved connection onto Bufstream's Azure data +// config, wiring env-var credential sources only when static keys are present. func renderAzureStorage(storage objectstore.ConnInfo, uri string) *bufstreamAzure { az := &bufstreamAzure{URI: uri} if storage.HasStaticCredentials() { diff --git a/internal/controller/infra/managed/kafka/bufstream/config_test.go b/internal/controller/infra/managed/kafka/bufstream/config_test.go index 4211a066..a65554b4 100644 --- a/internal/controller/infra/managed/kafka/bufstream/config_test.go +++ b/internal/controller/infra/managed/kafka/bufstream/config_test.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v3" apiv2 "github.com/wandb/operator/api/v2" - "github.com/wandb/operator/internal/controller/infra/external/objectstore" + "github.com/wandb/operator/internal/controller/infra/objectstore" ) func TestRenderBufstreamConfigS3(t *testing.T) { diff --git a/internal/controller/infra/managed/kafka/bufstream/spec.go b/internal/controller/infra/managed/kafka/bufstream/spec.go index c8ead740..fa49171a 100644 --- a/internal/controller/infra/managed/kafka/bufstream/spec.go +++ b/internal/controller/infra/managed/kafka/bufstream/spec.go @@ -5,7 +5,7 @@ import ( apiv2 "github.com/wandb/operator/api/v2" "github.com/wandb/operator/internal/controller/common" - "github.com/wandb/operator/internal/controller/infra/external/objectstore" + "github.com/wandb/operator/internal/controller/infra/objectstore" "github.com/wandb/operator/pkg/utils" "github.com/wandb/operator/pkg/wandb/manifest" corev1 "k8s.io/api/core/v1" @@ -58,6 +58,8 @@ func BucketEnsureImage(img manifest.ImageRef, globalImageRegistry string) string return resolveImage(img, globalImageRegistry, defaultBucketEnsureImage) } +// resolveImage returns the manifest-supplied image, falling back to the given +// default for older manifests that omit it. func resolveImage(img manifest.ImageRef, globalImageRegistry, fallback string) string { if out := img.GetImage(globalImageRegistry); out != "" { return out @@ -66,10 +68,12 @@ func resolveImage(img manifest.ImageRef, globalImageRegistry, fallback string) s return fallback } +// BuildWandbKafkaLabels returns the standard W&B labels for the Kafka module. func BuildWandbKafkaLabels(wandb *apiv2.WeightsAndBiases) map[string]string { return common.BuildWandbLabels(wandb, KafkaModuleName) } +// ToKafkaOnDeleteRule builds the on-delete retention rule for the Kafka module. func ToKafkaOnDeleteRule(wandb *apiv2.WeightsAndBiases, retentionPolicy apiv2.RetentionPolicy) common.OnDeleteRule { return common.ToOnDeleteRule(wandb, retentionPolicy, KafkaModuleName) } @@ -85,6 +89,8 @@ func kafkaPodSecurityContext() *corev1.PodSecurityContext { return bufstreamPodSecurityContext() } +// kafkaContainerSecurityContext returns the container security context, dropping +// the fixed UID/GID on OpenShift where the platform assigns them. func kafkaContainerSecurityContext() *corev1.SecurityContext { if utils.IsOpenShift() { return &corev1.SecurityContext{ @@ -109,6 +115,8 @@ func bufstreamPodSecurityContext() *corev1.PodSecurityContext { } } +// bufstreamContainerSecurityContext pins UID/GID 65532, which the broker's 0700 +// binary requires, and drops all capabilities. func bufstreamContainerSecurityContext() *corev1.SecurityContext { return &corev1.SecurityContext{ RunAsUser: ptr.To(kafkaRunAsUser), @@ -120,6 +128,7 @@ func bufstreamContainerSecurityContext() *corev1.SecurityContext { } } +// kafkaRuntimeDefaultSeccompProfile returns the RuntimeDefault seccomp profile. func kafkaRuntimeDefaultSeccompProfile() *corev1.SeccompProfile { return &corev1.SeccompProfile{Type: corev1.SeccompProfileTypeRuntimeDefault} } @@ -131,6 +140,8 @@ func sameNamespace(wandb *apiv2.WeightsAndBiases, nsnBuilder *NsNameBuilder) boo return wandb.Namespace == nsnBuilder.Namespace() } +// setOwner sets the WeightsAndBiases controller reference on obj, but only when +// it shares the CR's namespace, since owner references are namespace-scoped. func setOwner(wandb *apiv2.WeightsAndBiases, obj metav1.Object, nsnBuilder *NsNameBuilder, scheme *runtime.Scheme) error { if !sameNamespace(wandb, nsnBuilder) { return nil @@ -138,6 +149,7 @@ func setOwner(wandb *apiv2.WeightsAndBiases, obj metav1.Object, nsnBuilder *NsNa return ctrl.SetControllerReference(wandb, obj, scheme) } +// intstrFromInt converts a port number to an IntOrString for service/probe specs. func intstrFromInt(port int) intstr.IntOrString { return intstr.FromInt32(int32(port)) } @@ -425,7 +437,7 @@ func spreadAffinity(wandb *apiv2.WeightsAndBiases, spec apiv2.ManagedInfraSpec, func bucketEnsureContainer(nsnBuilder *NsNameBuilder, storage objectstore.ConnInfo, img manifest.ImageRef, globalImageRegistry string) corev1.Container { region := storage.Region if region == "" { - region = "us-east-1" + region = objectstore.DefaultRegion } credsName := nsnBuilder.CredentialsName() // Retry in this process so transient DNS and API startup failures do not @@ -444,7 +456,7 @@ exit 1`, bucketEnsureMaxAttempts, bucketEnsureMaxAttempts, bucketEnsureDelaySeco Name: "ensure-bucket", Image: BucketEnsureImage(img, globalImageRegistry), Command: []string{"/bin/sh", "-c"}, - Args: []string{script, "ensure-bucket", storage.Endpoint, storage.Bucket}, + Args: []string{script, "ensure-bucket", storage.EndpointURL(), storage.Bucket}, SecurityContext: kafkaContainerSecurityContext(), Env: []corev1.EnvVar{ {Name: "AWS_REGION", Value: region}, @@ -610,6 +622,8 @@ func ToBufstreamApplication( return app, nil } +// kafkaServiceAccountName returns the configured ServiceAccount name, defaulting +// to the spec name when unset. func kafkaServiceAccountName(spec *apiv2.ManagedKafkaSpec) string { if spec.ServiceAccount.ServiceAccountName != "" { return spec.ServiceAccount.ServiceAccountName diff --git a/internal/controller/infra/managed/kafka/bufstream/spec_test.go b/internal/controller/infra/managed/kafka/bufstream/spec_test.go index c0684ada..04551b09 100644 --- a/internal/controller/infra/managed/kafka/bufstream/spec_test.go +++ b/internal/controller/infra/managed/kafka/bufstream/spec_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" apiv2 "github.com/wandb/operator/api/v2" - "github.com/wandb/operator/internal/controller/infra/external/objectstore" + "github.com/wandb/operator/internal/controller/infra/objectstore" "github.com/wandb/operator/pkg/utils" "github.com/wandb/operator/pkg/wandb/manifest" corev1 "k8s.io/api/core/v1" @@ -104,11 +104,14 @@ func TestToEtcdApplicationHA(t *testing.T) { } func testStorage() objectstore.ConnInfo { + // Mirror the managed SeaweedFS shape: a bare host with the port/scheme carried + // separately, so EndpointURL() must reassemble "http://seaweedfs:8333". return objectstore.ConnInfo{ Provider: apiv2.ObjectStoreProviderS3, URI: "s3://bucket", Bucket: "bucket", - Endpoint: "http://seaweedfs:80", + Endpoint: "seaweedfs", + Port: "8333", Region: "us-east-1", AccessKey: "ak", SecretKey: "sk", @@ -138,7 +141,8 @@ func TestToBufstreamApplication(t *testing.T) { require.Contains(t, ensureBucket.Args[0], "-le 150") require.NotContains(t, ensureBucket.Args[0], testStorage().AccessKey) require.NotContains(t, ensureBucket.Args[0], testStorage().SecretKey) - require.Equal(t, testStorage().Endpoint, ensureBucket.Args[2]) + require.Equal(t, "http://seaweedfs:8333", ensureBucket.Args[2]) + require.Equal(t, testStorage().EndpointURL(), ensureBucket.Args[2]) require.Equal(t, testStorage().Bucket, ensureBucket.Args[3]) require.Equal(t, int32(2), *app.Spec.Replicas) require.Len(t, app.Spec.PodTemplate.Spec.Containers, 1) diff --git a/internal/controller/infra/managed/kafka/bufstream/write.go b/internal/controller/infra/managed/kafka/bufstream/write.go index d1e8cc81..27a916a3 100644 --- a/internal/controller/infra/managed/kafka/bufstream/write.go +++ b/internal/controller/infra/managed/kafka/bufstream/write.go @@ -2,11 +2,10 @@ package bufstream import ( "context" - "strconv" apiv2 "github.com/wandb/operator/api/v2" "github.com/wandb/operator/internal/controller/common" - "github.com/wandb/operator/internal/controller/infra/external/objectstore" + "github.com/wandb/operator/internal/controller/infra/objectstore" "github.com/wandb/operator/internal/logx" "github.com/wandb/operator/pkg/utils" "github.com/wandb/operator/pkg/wandb/manifest" @@ -98,6 +97,7 @@ func WriteState( return results } +// translateError wraps an error as a failed Reconciled condition. func translateError(err error) []metav1.Condition { return []metav1.Condition{ {Type: common.ReconciledType, Status: metav1.ConditionFalse, Reason: common.ControllerErrorReason, Message: err.Error()}, @@ -141,6 +141,7 @@ func writeResource[T client.Object]( return []metav1.Condition{actionToCondition(conditionType, action)} } +// actionToCondition maps a CRUD action onto the status condition it implies. func actionToCondition(conditionType string, action common.CrudAction) metav1.Condition { switch action { case common.CreateAction: @@ -173,70 +174,9 @@ func resolveStorage( return objectstore.ConnInfo{}, false, nil } - resolver := &utils.ConnSecretResolver{Client: cl, Namespace: spec.Namespace, Cache: map[string]*corev1.Secret{}} - - connInfo := objectstore.ConnInfo{} - - provider, err := resolver.Value(ctx, status.Connection.Provider) - if err != nil { - return objectstore.ConnInfo{}, true, err - } - connInfo.Provider = apiv2.ObjectStoreProvider(provider) - - connInfo.Bucket, err = resolver.Value(ctx, status.Connection.Bucket) - if err != nil { - return objectstore.ConnInfo{}, true, err - } - - connInfo.Endpoint, err = resolver.Value(ctx, status.Connection.Endpoint) - if err != nil { - return objectstore.ConnInfo{}, true, err - } - - connInfo.Port, err = resolver.Value(ctx, status.Connection.Port) - if err != nil { - return objectstore.ConnInfo{}, true, err - } - - connInfo.Region, err = resolver.Value(ctx, status.Connection.Region) - if err != nil { - return objectstore.ConnInfo{}, true, err - } - - connInfo.AccessKey, err = resolver.Value(ctx, status.Connection.AccessKey) - if err != nil { - return objectstore.ConnInfo{}, true, err - } - - connInfo.SecretKey, err = resolver.Value(ctx, status.Connection.SecretKey) + connInfo, err := objectstore.Resolve(ctx, cl, spec.Namespace, &status.Connection) if err != nil { return objectstore.ConnInfo{}, true, err } - - forcePathStyleString, err := resolver.Value(ctx, status.Connection.ForcePathStyle) - if err != nil { - return objectstore.ConnInfo{}, true, err - } - if fps, parseErr := strconv.ParseBool(forcePathStyleString); parseErr == nil { - connInfo.ForcePathStyle = fps - } else { - // Connection secrets written before the operator derived this key lack it. - connInfo.ForcePathStyle = objectstore.RequiresPathStyle(connInfo.Endpoint) - } - - tlsEnabledString, err := resolver.Value(ctx, status.Connection.TlsEnabled) - if err != nil { - return objectstore.ConnInfo{}, true, err - } - connInfo.TlsEnabled, err = strconv.ParseBool(tlsEnabledString) - if err != nil { - connInfo.TlsEnabled = false - } - - connInfo.Path, err = resolver.Value(ctx, status.Connection.Path) - if err != nil { - return objectstore.ConnInfo{}, true, err - } - return connInfo, true, nil } diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/conn.go b/internal/controller/infra/managed/objectstore/seaweedfs/conn.go index af96891d..cf5b577a 100644 --- a/internal/controller/infra/managed/objectstore/seaweedfs/conn.go +++ b/internal/controller/infra/managed/objectstore/seaweedfs/conn.go @@ -4,10 +4,10 @@ import ( "context" "fmt" "net/url" - "strconv" apiv2 "github.com/wandb/operator/api/v2" "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/objectstore" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -20,30 +20,40 @@ const ( S3Port = "8333" ) -type s3ConnInfo struct { - AccessKey string - SecretKey string - Host string - Port string - Bucket string - TLS bool - ForcePathStyle bool -} - +// buildS3ConnInfo assembles the ConnInfo for the managed SeaweedFS S3 gateway: +// the in-cluster service host/port, path-style addressing, and default region. func buildS3ConnInfo( accessKey, secretKey string, nsnBuilder *NsNameBuilder, tls bool, -) *s3ConnInfo { - return &s3ConnInfo{ +) *objectstore.ConnInfo { + connInfo := &objectstore.ConnInfo{ + Provider: apiv2.ObjectStoreProviderS3, AccessKey: accessKey, - TLS: tls, SecretKey: secretKey, - Host: s3ServiceHost(nsnBuilder.SpecName(), nsnBuilder.Namespace()), + Endpoint: s3ServiceHost(nsnBuilder.SpecName(), nsnBuilder.Namespace()), Port: S3Port, + Region: objectstore.DefaultRegion, Bucket: "bucket", + Scheme: objectstore.SchemeForTLS(tls), + TlsEnabled: tls, ForcePathStyle: true, } + connInfo.URL = managedS3URL(connInfo) + return connInfo +} + +// managedS3URL builds the canonical connection URL the W&B server signs against: +// s3://<accessKey>:<secretKey>@<host>:<port>/<bucket>?tls=<bool>. +func managedS3URL(connInfo *objectstore.ConnInfo) string { + s3URL := &url.URL{ + Scheme: S3UrlScheme, + Host: fmt.Sprintf("%s:%s", connInfo.Endpoint, connInfo.Port), + User: url.UserPassword(connInfo.AccessKey, connInfo.SecretKey), + Path: connInfo.Bucket, + } + return fmt.Sprintf("%s?tls=%t", s3URL.String(), connInfo.TlsEnabled) } +// s3ServiceHost returns the in-cluster FQDN of the SeaweedFS S3 service. func s3ServiceHost(specName, namespace string) string { return fmt.Sprintf("%s-s3.%s.svc.cluster.local", SeaweedName(specName), namespace) } @@ -53,35 +63,18 @@ func s3ServiceHost(specName, namespace string) string { // without re-signing). The s3 gateway must verify signatures against this // host rather than the Host/X-Forwarded-Host of proxied requests. func s3ExternalURL(specName, namespace string, tls bool) string { - return fmt.Sprintf("%s://%s:%s", s3Scheme(tls), s3ServiceHost(specName, namespace), S3Port) -} - -func s3Scheme(tls bool) string { - if tls { - return "https" - } - return "http" -} - -func (s *s3ConnInfo) toUrl() *url.URL { - return &url.URL{ - Scheme: S3UrlScheme, - Host: fmt.Sprintf("%s:%s", s.Host, s.Port), - User: url.UserPassword(s.AccessKey, s.SecretKey), - Path: s.Bucket, - } -} - -func (s *s3ConnInfo) scheme() string { - return s3Scheme(s.TLS) + return fmt.Sprintf("%s://%s:%s", objectstore.SchemeForTLS(tls), s3ServiceHost(specName, namespace), S3Port) } +// writeWandbConnInfo writes the connection secret consumed by W&B and returns +// the ObjectStoreConnection with every selector required, since managed +// SeaweedFS always persists the full key set. func writeWandbConnInfo( ctx context.Context, cl client.Client, owner client.Object, nsnBuilder *NsNameBuilder, - connInfo *s3ConnInfo, + connInfo *objectstore.ConnInfo, ) ( *apiv2.ObjectStoreConnection, error, ) { @@ -91,7 +84,6 @@ func writeWandbConnInfo( var actual = &corev1.Secret{} nsName := nsnBuilder.ConnectionNsName() - urlKey := "url" if found, err = common.GetResource( ctx, cl, nsName, AppConnTypeName, actual, @@ -120,37 +112,14 @@ func writeWandbConnInfo( Namespace: nsName.Namespace, OwnerReferences: []metav1.OwnerReference{ref}, }, - Type: corev1.SecretTypeOpaque, - StringData: map[string]string{ - urlKey: fmt.Sprintf("%s?tls=%t", connInfo.toUrl().String(), connInfo.TLS), - "Host": connInfo.Host, - "Port": connInfo.Port, - "AccessKey": connInfo.AccessKey, - "SecretKey": connInfo.SecretKey, - "Region": "us-east-1", - "Bucket": connInfo.Bucket, - "Scheme": connInfo.scheme(), - "TlsEnabled": strconv.FormatBool(connInfo.TLS), - "Provider": "s3", - "ForcePathStyle": strconv.FormatBool(connInfo.ForcePathStyle), - }, + Type: corev1.SecretTypeOpaque, + StringData: connInfo.ToSecretData(), } if _, err = common.CrudResource(ctx, cl, desired, actual); err != nil { return nil, err } - localRef := corev1.LocalObjectReference{Name: nsName.Name} - return &apiv2.ObjectStoreConnection{ - URL: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: urlKey, Optional: ptr.To(false)}, - Endpoint: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Host", Optional: ptr.To(false)}, - Port: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Port", Optional: ptr.To(false)}, - AccessKey: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "AccessKey", Optional: ptr.To(false)}, - SecretKey: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "SecretKey", Optional: ptr.To(false)}, - Region: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Region", Optional: ptr.To(false)}, - Bucket: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Bucket", Optional: ptr.To(false)}, - TlsEnabled: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "TlsEnabled", Optional: ptr.To(false)}, - Provider: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "Provider", Optional: ptr.To(false)}, - ForcePathStyle: corev1.SecretKeySelector{LocalObjectReference: localRef, Key: "ForcePathStyle", Optional: ptr.To(false)}, - }, nil + // Managed SeaweedFS always writes the full key set, so every selector is required. + return connInfo.ToObjectStoreConnection(nsName.Name, true), nil } diff --git a/internal/controller/infra/managed/objectstore/seaweedfs/write.go b/internal/controller/infra/managed/objectstore/seaweedfs/write.go index 6912c0ae..7daac715 100644 --- a/internal/controller/infra/managed/objectstore/seaweedfs/write.go +++ b/internal/controller/infra/managed/objectstore/seaweedfs/write.go @@ -7,6 +7,7 @@ import ( "github.com/Masterminds/goutils" apiv2 "github.com/wandb/operator/api/v2" "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/objectstore" "github.com/wandb/operator/internal/logx" seaweedv1 "github.com/wandb/operator/pkg/vendored/seaweedfs-operator/seaweed.seaweedfs.com/v1" corev1 "k8s.io/api/core/v1" @@ -23,6 +24,9 @@ const ( AppConnTypeName = "SeaweedAppConn" ) +// WriteState reconciles the managed SeaweedFS CR, its S3 identity config, and +// the W&B connection secret, returning the reconcile conditions plus the +// resulting ObjectStoreConnection (nil until the connection is available). func WriteState( ctx context.Context, kubeClient client.Client, @@ -138,13 +142,16 @@ func WriteState( return result, nil } +// writeSeaweedS3Config persists the SeaweedFS S3 identity config secret, +// preserving the existing secret key when one is already present so credentials +// stay stable across reconciles, and returns the resolved ConnInfo. func writeSeaweedS3Config( ctx context.Context, client client.Client, owner *seaweedv1.Seaweed, nsnBuilder *NsNameBuilder, envConfig SeaweedS3Config, -) (*s3ConnInfo, error) { +) (*objectstore.ConnInfo, error) { var err error var found bool var gvk schema.GroupVersionKind diff --git a/internal/controller/infra/objectstore/conn.go b/internal/controller/infra/objectstore/conn.go new file mode 100644 index 00000000..54048b65 --- /dev/null +++ b/internal/controller/infra/objectstore/conn.go @@ -0,0 +1,65 @@ +// Package objectstore holds the unified object-store connection model shared by +// the managed (SeaweedFS) and external reconcile paths: a single ConnInfo struct +// plus mapper functions that convert between it, the connection Secret's data, +// and the apiv2.ObjectStoreConnection selectors. +package objectstore + +import ( + apiv2 "github.com/wandb/operator/api/v2" + corev1 "k8s.io/api/core/v1" +) + +// DefaultRegion is the region assumed when a connection carries none; S3 SDKs and +// S3-compatible backends (SeaweedFS, MinIO) require some region to be set. +const DefaultRegion = "us-east-1" + +// ConnInfo is the resolved object-store connection: the read-side counterpart to +// the connection secret, and the value both write paths populate before encoding +// it back out via ToSecretData / ToObjectStoreConnection. +type ConnInfo struct { + Provider apiv2.ObjectStoreProvider + // URI is the provider-native location, e.g. "s3://bucket", "gs://bucket/prefix", or "https://acct.blob.core.windows.net/container". + URI string + // URL is the canonical connection URL persisted under the secret's "url" key. + // It is built per-flow (managed appends "?tls=<bool>"; external is provider-specific), so ToSecretData serializes it verbatim. + URL string + // Bucket is the bare bucket/container name. + Bucket string + // Endpoint is the S3 API endpoint for S3-compatible providers (SeaweedFS, MinIO); empty for AWS S3, GCS, and Azure. It maps to the secret's "Host" key. + Endpoint string + Region string + AccessKey string + SecretKey string + // Scheme is the object-store URL scheme ("http"/"https"); only set by the managed path, persisted under the secret's "Scheme" key. + Scheme string + // ForcePathStyle is required by most non-AWS S3-compatible providers. + ForcePathStyle bool + TlsEnabled bool + Port string + Path string + // Credential selectors, kept for consumers that inject creds by reference. + AccessKeyRef corev1.SecretKeySelector + SecretKeyRef corev1.SecretKeySelector +} + +// HasStaticCredentials reports whether explicit keys were provided; when false, credentials come from ambient identity (IAM role / workload identity). +func (c ConnInfo) HasStaticCredentials() bool { + return c.AccessKey != "" && c.SecretKey != "" +} + +// ProviderURI builds the provider-native base URI for the connection's bucket: +// "s3://<bucket>", "gs://<bucket>", or the Azure blob container URL. It returns "" +// for an unknown provider. Any object prefix is the caller's to append. +func (c ConnInfo) ProviderURI() string { + switch c.Provider { + case apiv2.ObjectStoreProviderS3: + return "s3://" + c.Bucket + case apiv2.ObjectStoreProviderGCS: + return "gs://" + c.Bucket + case apiv2.ObjectStoreProviderAzure: + // Azure carries the storage account in AccessKey. + return AzureBlobURI(c.AccessKey, c.Bucket, "") + default: + return "" + } +} diff --git a/internal/controller/infra/objectstore/endpoint.go b/internal/controller/infra/objectstore/endpoint.go new file mode 100644 index 00000000..c54cd612 --- /dev/null +++ b/internal/controller/infra/objectstore/endpoint.go @@ -0,0 +1,51 @@ +package objectstore + +import ( + "fmt" + "strings" +) + +// SchemeForTLS returns the URL scheme implied by whether TLS is enabled. +func SchemeForTLS(tls bool) string { + if tls { + return "https" + } + return "http" +} + +// SplitScheme separates a "scheme://rest" endpoint into its scheme and remainder. +// When no scheme is present it returns an empty scheme and the input unchanged. +func SplitScheme(endpoint string) (scheme, rest string) { + if i := strings.Index(endpoint, "://"); i >= 0 { + return endpoint[:i], endpoint[i+len("://"):] + } + return "", endpoint +} + +// EndpointURL renders the S3-compatible API endpoint as "scheme://host[:port]", +// or "" when no endpoint override is set (i.e. plain AWS S3). A scheme already +// present in Endpoint is preserved; otherwise it is derived from TlsEnabled. Port +// is appended only when set and the host does not already carry one. +func (c ConnInfo) EndpointURL() string { + if c.Endpoint == "" { + return "" + } + scheme, host := SplitScheme(c.Endpoint) + if scheme == "" { + scheme = SchemeForTLS(c.TlsEnabled) + } + if c.Port != "" && !strings.Contains(host, ":") { + host += ":" + c.Port + } + return fmt.Sprintf("%s://%s", scheme, host) +} + +// AzureBlobURI builds the Azure Blob container URL for a storage account, e.g. +// "https://<account>.blob.core.windows.net/<container>[/<prefix>]". +func AzureBlobURI(account, container, prefix string) string { + uri := fmt.Sprintf("https://%s.blob.core.windows.net/%s", account, container) + if prefix != "" { + uri += "/" + prefix + } + return uri +} diff --git a/internal/controller/infra/objectstore/endpoint_test.go b/internal/controller/infra/objectstore/endpoint_test.go new file mode 100644 index 00000000..c7c5fb84 --- /dev/null +++ b/internal/controller/infra/objectstore/endpoint_test.go @@ -0,0 +1,62 @@ +package objectstore + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSchemeForTLS(t *testing.T) { + require.Equal(t, "https", SchemeForTLS(true)) + require.Equal(t, "http", SchemeForTLS(false)) +} + +func TestSplitScheme(t *testing.T) { + cases := []struct { + in string + wantScheme string + wantRest string + }{ + {"https://minio.example.com:9000", "https", "minio.example.com:9000"}, + {"http://host", "http", "host"}, + {"host:9000", "", "host:9000"}, + {"host", "", "host"}, + {"", "", ""}, + } + for _, tc := range cases { + t.Run(tc.in, func(t *testing.T) { + scheme, rest := SplitScheme(tc.in) + require.Equal(t, tc.wantScheme, scheme) + require.Equal(t, tc.wantRest, rest) + }) + } +} + +func TestEndpointURL(t *testing.T) { + cases := []struct { + name string + ci ConnInfo + want string + }{ + {"no endpoint (AWS S3)", ConnInfo{}, ""}, + {"host with port, tls off", ConnInfo{Endpoint: "minio.example.com", Port: "9000"}, "http://minio.example.com:9000"}, + {"host with port, tls on", ConnInfo{Endpoint: "minio.example.com", Port: "9000", TlsEnabled: true}, "https://minio.example.com:9000"}, + {"host without port", ConnInfo{Endpoint: "minio.example.com", TlsEnabled: true}, "https://minio.example.com"}, + {"scheme preserved over tls", ConnInfo{Endpoint: "http://seaweedfs.svc", Port: "80", TlsEnabled: true}, "http://seaweedfs.svc:80"}, + {"scheme and port already in endpoint", ConnInfo{Endpoint: "http://objstore.svc:8333"}, "http://objstore.svc:8333"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.want, tc.ci.EndpointURL()) + }) + } +} + +func TestAzureBlobURI(t *testing.T) { + require.Equal(t, + "https://acct.blob.core.windows.net/container", + AzureBlobURI("acct", "container", "")) + require.Equal(t, + "https://acct.blob.core.windows.net/container/some/prefix", + AzureBlobURI("acct", "container", "some/prefix")) +} diff --git a/internal/controller/infra/external/objectstore/pathstyle.go b/internal/controller/infra/objectstore/pathstyle.go similarity index 100% rename from internal/controller/infra/external/objectstore/pathstyle.go rename to internal/controller/infra/objectstore/pathstyle.go diff --git a/internal/controller/infra/external/objectstore/pathstyle_test.go b/internal/controller/infra/objectstore/pathstyle_test.go similarity index 100% rename from internal/controller/infra/external/objectstore/pathstyle_test.go rename to internal/controller/infra/objectstore/pathstyle_test.go diff --git a/internal/controller/infra/objectstore/resolve_test.go b/internal/controller/infra/objectstore/resolve_test.go new file mode 100644 index 00000000..b8103f74 --- /dev/null +++ b/internal/controller/infra/objectstore/resolve_test.go @@ -0,0 +1,163 @@ +package objectstore + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + apiv2 "github.com/wandb/operator/api/v2" +) + +const connSecretName = "wandb-objectstore-connection" + +// resolveFixture builds a fake client holding a single connection secret with +// the given keys and an ObjectStoreConnection whose selectors point at them. +func resolveFixture(t *testing.T, data map[string]string) (*apiv2.ObjectStoreConnection, ConnInfo, error) { + t.Helper() + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + + raw := map[string][]byte{} + for k, v := range data { + raw[k] = []byte(v) + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: connSecretName, Namespace: "default"}, + Data: raw, + } + + connSel := func(key string) corev1.SecretKeySelector { + return corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: connSecretName}, + Key: key, + } + } + conn := &apiv2.ObjectStoreConnection{ + Provider: connSel("Provider"), + Endpoint: connSel("Host"), + Port: connSel("Port"), + AccessKey: connSel("AccessKey"), + SecretKey: connSel("SecretKey"), + Bucket: connSel("Bucket"), + Path: connSel("Path"), + Region: connSel("Region"), + TlsEnabled: connSel("TlsEnabled"), + ForcePathStyle: connSel("ForcePathStyle"), + } + + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(secret).Build() + info, err := Resolve(context.Background(), c, "default", conn) + return conn, info, err +} + +func TestResolve_NilConnection(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + c := fake.NewClientBuilder().WithScheme(scheme).Build() + + _, err := Resolve(context.Background(), c, "default", nil) + require.Error(t, err) +} + +func TestResolve_ExternalS3WithStaticCredentials(t *testing.T) { + conn, info, err := resolveFixture(t, map[string]string{ + "Provider": "s3", + "Host": "minio.local", + "Port": "9000", + "AccessKey": "minio", + "SecretKey": "minio123", + "Bucket": "my-bucket", + "Path": "team/prefix", + "Region": "us-east-1", + "TlsEnabled": "true", + "ForcePathStyle": "true", + }) + require.NoError(t, err) + + require.Equal(t, apiv2.ObjectStoreProviderS3, info.Provider) + require.Equal(t, "minio.local", info.Endpoint) + require.Equal(t, "9000", info.Port) + require.Equal(t, "minio", info.AccessKey) + require.Equal(t, "minio123", info.SecretKey) + require.Equal(t, "my-bucket", info.Bucket) + require.Equal(t, "team/prefix", info.Path) + require.Equal(t, "us-east-1", info.Region) + require.True(t, info.TlsEnabled) + require.True(t, info.ForcePathStyle) + require.True(t, info.HasStaticCredentials()) + + // The credential selectors are preserved for consumers that inject by ref. + require.Equal(t, conn.AccessKey, info.AccessKeyRef) + require.Equal(t, conn.SecretKey, info.SecretKeyRef) +} + +func TestResolve_AmbientCredentials(t *testing.T) { + _, info, err := resolveFixture(t, map[string]string{ + "Provider": "s3", + "Bucket": "my-bucket", + "Region": "us-west-2", + }) + require.NoError(t, err) + + require.Empty(t, info.AccessKey) + require.Empty(t, info.SecretKey) + require.False(t, info.HasStaticCredentials()) + require.Equal(t, "us-west-2", info.Region) +} + +func TestResolve_ForcePathStyleFallbackFromEndpoint(t *testing.T) { + // A custom endpoint with no ForcePathStyle key falls back to RequiresPathStyle. + _, info, err := resolveFixture(t, map[string]string{ + "Host": "minio.local", + "Bucket": "my-bucket", + }) + require.NoError(t, err) + require.True(t, info.ForcePathStyle) + + // No endpoint (native AWS) means virtual-hosted addressing. + _, info, err = resolveFixture(t, map[string]string{ + "Bucket": "my-bucket", + }) + require.NoError(t, err) + require.False(t, info.ForcePathStyle) +} + +func TestResolve_DefaultsEmptyProviderToS3(t *testing.T) { + // Legacy-migrated connections omit Provider; Resolve must default it to S3 so + // downstream provider switches (ProviderURI, ToSecretData) don't fall through. + _, info, err := resolveFixture(t, map[string]string{ + "Bucket": "my-bucket", + }) + require.NoError(t, err) + require.Equal(t, apiv2.ObjectStoreProviderS3, info.Provider) +} + +func TestResolve_RejectsPartialCredentialPair(t *testing.T) { + // Access key without a secret key. + _, _, err := resolveFixture(t, map[string]string{ + "Bucket": "my-bucket", + "AccessKey": "only-access", + }) + require.Error(t, err) + + // Secret key without an access key. + _, _, err = resolveFixture(t, map[string]string{ + "Bucket": "my-bucket", + "SecretKey": "only-secret", + }) + require.Error(t, err) +} + +func TestResolve_MissingTlsDefaultsFalse(t *testing.T) { + _, info, err := resolveFixture(t, map[string]string{ + "Host": "minio.local", + "Bucket": "my-bucket", + }) + require.NoError(t, err) + require.False(t, info.TlsEnabled) +} diff --git a/internal/controller/infra/objectstore/secret.go b/internal/controller/infra/objectstore/secret.go new file mode 100644 index 00000000..2186439b --- /dev/null +++ b/internal/controller/infra/objectstore/secret.go @@ -0,0 +1,185 @@ +package objectstore + +import ( + "context" + "fmt" + "strconv" + "strings" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/pkg/utils" + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// connectionRequiredKeys are the secret keys every consumer must be able to read; +// their selectors are never marked optional regardless of the optionality policy. +var connectionRequiredKeys = map[string]bool{"url": true, "Provider": true, "Bucket": true} + +// ToSecretData encodes the connection into the Secret's StringData: the canonical +// "url", the discrete keys consumers read, and the bool flags. Empty discrete +// values are omitted so absent fields stay absent (matching the resolver's +// treatment of missing keys). ForcePathStyle is S3-only. +func (c ConnInfo) ToSecretData() map[string]string { + data := map[string]string{"url": c.URL} + + put := func(key, value string) { + if value != "" { + data[key] = value + } + } + put("Provider", string(c.Provider)) + put("Bucket", c.Bucket) + put("Host", c.Endpoint) + put("Port", c.Port) + put("AccessKey", c.AccessKey) + put("SecretKey", c.SecretKey) + put("Region", c.Region) + put("Path", c.Path) + put("Scheme", c.Scheme) + + data["TlsEnabled"] = strconv.FormatBool(c.TlsEnabled) + if c.Provider == apiv2.ObjectStoreProviderS3 { + data["ForcePathStyle"] = strconv.FormatBool(c.ForcePathStyle) + } + return data +} + +// ToObjectStoreConnection builds the selector view of the connection secret named +// secretName. It emits a selector only for keys ToSecretData actually writes. +// When requireAll is true every selector is required (managed SeaweedFS always +// writes the full key set); otherwise only url/Provider/Bucket are required and +// the rest are optional (external configs omit provider-dependent keys). +func (c ConnInfo) ToObjectStoreConnection(secretName string, requireAll bool) *apiv2.ObjectStoreConnection { + data := c.ToSecretData() + localRef := corev1.LocalObjectReference{Name: secretName} + + sel := func(key string) corev1.SecretKeySelector { + optional := !requireAll && !connectionRequiredKeys[key] + return corev1.SecretKeySelector{LocalObjectReference: localRef, Key: key, Optional: ptr.To(optional)} + } + has := func(key string) bool { _, ok := data[key]; return ok } + + conn := &apiv2.ObjectStoreConnection{} + if has("url") { + conn.URL = sel("url") + } + if has("Provider") { + conn.Provider = sel("Provider") + } + if has("Host") { + conn.Endpoint = sel("Host") + } + if has("Port") { + conn.Port = sel("Port") + } + if has("AccessKey") { + conn.AccessKey = sel("AccessKey") + } + if has("SecretKey") { + conn.SecretKey = sel("SecretKey") + } + if has("Bucket") { + conn.Bucket = sel("Bucket") + } + if has("Path") { + conn.Path = sel("Path") + } + if has("Region") { + conn.Region = sel("Region") + } + if has("TlsEnabled") { + conn.TlsEnabled = sel("TlsEnabled") + } + if has("ForcePathStyle") { + conn.ForcePathStyle = sel("ForcePathStyle") + } + return conn +} + +// Resolve reads the connection's secret selectors into a ConnInfo. +func Resolve( + ctx context.Context, + cl client.Client, + namespace string, + conn *apiv2.ObjectStoreConnection, +) (ConnInfo, error) { + if conn == nil { + return ConnInfo{}, fmt.Errorf("object store connection is not available yet") + } + + resolver := &utils.ConnSecretResolver{Client: cl, Namespace: namespace, Cache: map[string]*corev1.Secret{}} + + info := ConnInfo{ + AccessKeyRef: conn.AccessKey, + SecretKeyRef: conn.SecretKey, + } + + provider, err := resolver.Value(ctx, conn.Provider) + if err != nil { + return ConnInfo{}, err + } + // Legacy-migrated connections never set Provider; match WriteState's default so + // downstream provider switches (ProviderURI, ToSecretData) stay correct. + if provider == "" { + provider = string(apiv2.ObjectStoreProviderS3) + } + info.Provider = apiv2.ObjectStoreProvider(provider) + + if info.Bucket, err = resolver.Value(ctx, conn.Bucket); err != nil { + return ConnInfo{}, err + } + if info.Endpoint, err = resolver.Value(ctx, conn.Endpoint); err != nil { + return ConnInfo{}, err + } + if info.Port, err = resolver.Value(ctx, conn.Port); err != nil { + return ConnInfo{}, err + } + if info.Region, err = resolver.Value(ctx, conn.Region); err != nil { + return ConnInfo{}, err + } + if info.AccessKey, err = resolver.Value(ctx, conn.AccessKey); err != nil { + return ConnInfo{}, err + } + if info.SecretKey, err = resolver.Value(ctx, conn.SecretKey); err != nil { + return ConnInfo{}, err + } + // A half-configured pair silently picks the wrong credential mode downstream. + if (info.AccessKey == "") != (info.SecretKey == "") { + return ConnInfo{}, fmt.Errorf("object store access key and secret key must be configured together") + } + if info.Path, err = resolver.Value(ctx, conn.Path); err != nil { + return ConnInfo{}, err + } + + forcePathStyleString, err := resolver.Value(ctx, conn.ForcePathStyle) + if err != nil { + return ConnInfo{}, err + } + if fps, parseErr := strconv.ParseBool(forcePathStyleString); parseErr == nil { + info.ForcePathStyle = fps + } else { + // Connection secrets written before the operator derived this key lack it. + info.ForcePathStyle = RequiresPathStyle(info.Endpoint) + } + + tlsEnabledString, err := resolver.Value(ctx, conn.TlsEnabled) + if err != nil { + return ConnInfo{}, err + } + if tls, parseErr := strconv.ParseBool(tlsEnabledString); parseErr == nil { + info.TlsEnabled = tls + } + + return info, nil +} + +// SplitBucketPath splits "bucket/optional/prefix" into the leading bucket (or container) segment and the remaining object prefix. +func SplitBucketPath(raw string) (bucket, path string) { + trimmed := strings.TrimPrefix(raw, "/") + if slash := strings.IndexByte(trimmed, '/'); slash >= 0 { + return trimmed[:slash], trimmed[slash+1:] + } + return trimmed, "" +} diff --git a/internal/controller/infra/objectstore/secret_test.go b/internal/controller/infra/objectstore/secret_test.go new file mode 100644 index 00000000..9e0883ad --- /dev/null +++ b/internal/controller/infra/objectstore/secret_test.go @@ -0,0 +1,123 @@ +package objectstore + +import ( + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + + apiv2 "github.com/wandb/operator/api/v2" +) + +func TestToSecretData_ManagedSeaweedShape(t *testing.T) { + // Mirrors the values the managed SeaweedFS path populates; the resulting + // secret must contain exactly the historical key set (no Path). + ci := ConnInfo{ + Provider: apiv2.ObjectStoreProviderS3, + URL: "s3://ak:sk@object-store-s3.wandb.svc.cluster.local:8333/bucket?tls=false", + Endpoint: "object-store-s3.wandb.svc.cluster.local", + Port: "8333", + AccessKey: "ak", + SecretKey: "sk", + Region: "us-east-1", + Bucket: "bucket", + Scheme: "http", + TlsEnabled: false, + ForcePathStyle: true, + } + + require.Equal(t, map[string]string{ + "url": "s3://ak:sk@object-store-s3.wandb.svc.cluster.local:8333/bucket?tls=false", + "Host": "object-store-s3.wandb.svc.cluster.local", + "Port": "8333", + "AccessKey": "ak", + "SecretKey": "sk", + "Region": "us-east-1", + "Bucket": "bucket", + "Scheme": "http", + "TlsEnabled": "false", + "Provider": "s3", + "ForcePathStyle": "true", + }, ci.ToSecretData()) +} + +func TestToSecretData_OmitsEmptyDiscreteKeys(t *testing.T) { + ci := ConnInfo{ + Provider: apiv2.ObjectStoreProviderS3, + URL: "s3://my-bucket", + Bucket: "my-bucket", + } + data := ci.ToSecretData() + require.Equal(t, "s3://my-bucket", data["url"]) + require.NotContains(t, data, "Host") + require.NotContains(t, data, "AccessKey") + require.NotContains(t, data, "SecretKey") + require.NotContains(t, data, "Region") + require.NotContains(t, data, "Path") + require.NotContains(t, data, "Scheme") + // TlsEnabled is always written; ForcePathStyle is written for S3. + require.Equal(t, "false", data["TlsEnabled"]) + require.Equal(t, "false", data["ForcePathStyle"]) +} + +func TestToSecretData_ForcePathStyleIsS3Only(t *testing.T) { + for _, provider := range []apiv2.ObjectStoreProvider{apiv2.ObjectStoreProviderGCS, apiv2.ObjectStoreProviderAzure} { + ci := ConnInfo{Provider: provider, URL: "gs://b", Bucket: "b"} + require.NotContains(t, ci.ToSecretData(), "ForcePathStyle", "path-style is an S3-only concept") + } +} + +func TestToObjectStoreConnection_RequireAll(t *testing.T) { + ci := ConnInfo{ + Provider: apiv2.ObjectStoreProviderS3, + URL: "s3://ak:sk@host:8333/bucket?tls=false", + Endpoint: "host", + Port: "8333", + AccessKey: "ak", + SecretKey: "sk", + Region: "us-east-1", + Bucket: "bucket", + Scheme: "http", + ForcePathStyle: true, + } + conn := ci.ToObjectStoreConnection("conn-secret", true) + + // Every emitted selector points at conn-secret and is required. + for _, s := range []corev1.SecretKeySelector{ + conn.URL, conn.Provider, conn.Endpoint, conn.Port, conn.AccessKey, + conn.SecretKey, conn.Region, conn.Bucket, conn.TlsEnabled, conn.ForcePathStyle, + } { + require.Equal(t, "conn-secret", s.Name) + require.NotNil(t, s.Optional) + require.False(t, *s.Optional) + } + require.Equal(t, "Host", conn.Endpoint.Key) + require.Equal(t, "url", conn.URL.Key) + // Path is not written for the managed shape, so its selector stays empty. + require.Empty(t, conn.Path.Name) +} + +func TestToObjectStoreConnection_ExternalOptionality(t *testing.T) { + ci := ConnInfo{ + Provider: apiv2.ObjectStoreProviderS3, + URL: "s3://ak:sk@host:9000/bucket", + Endpoint: "host", + Port: "9000", + AccessKey: "ak", + SecretKey: "sk", + Region: "us-west-2", + Bucket: "bucket", + } + conn := ci.ToObjectStoreConnection("conn-secret", false) + + // url/Provider/Bucket are required... + for _, s := range []corev1.SecretKeySelector{conn.URL, conn.Provider, conn.Bucket} { + require.NotNil(t, s.Optional) + require.False(t, *s.Optional) + } + // ...everything else is optional. + for _, s := range []corev1.SecretKeySelector{conn.Endpoint, conn.Port, conn.AccessKey, conn.SecretKey, conn.Region} { + require.NotNil(t, s.Optional) + require.True(t, *s.Optional) + } +} diff --git a/internal/controller/reconciler/clickhouse.go b/internal/controller/reconciler/clickhouse.go index ca6dc153..f842da17 100644 --- a/internal/controller/reconciler/clickhouse.go +++ b/internal/controller/reconciler/clickhouse.go @@ -180,7 +180,7 @@ func managedClickHouseWriteState( waitForObjectStore := objStoreSpec.ManagedObjectStore != nil // Resolve the bucket connection; wait and requeue if it isn't ready yet. - objStorage, err := altinity.ResolveObjectStorage(ctx, client, spec, &objStoreStatus.Connection) + objStorage, objStorageEndpoint, err := altinity.ResolveObjectStorage(ctx, client, spec, &objStoreStatus.Connection) if err != nil { log.Error(err, "object storage not ready for ClickHouse") return []metav1.Condition{ @@ -222,7 +222,7 @@ func managedClickHouseWriteState( } } - desired, err := altinity.ToClickHouseVendorSpec(ctx, wandb, spec, client.Scheme(), objStorage, waitForObjectStore, mfst) + desired, err := altinity.ToClickHouseVendorSpec(ctx, wandb, spec, client.Scheme(), objStorage, objStorageEndpoint, waitForObjectStore, mfst) if err != nil { log.Error(err, "failed to translate ClickHouse spec to vendor spec") return []metav1.Condition{ diff --git a/internal/controller/reconciler/migrate_legacy.go b/internal/controller/reconciler/migrate_legacy.go index 1fc33318..5b4fc865 100644 --- a/internal/controller/reconciler/migrate_legacy.go +++ b/internal/controller/reconciler/migrate_legacy.go @@ -32,7 +32,7 @@ import ( apiv1 "github.com/wandb/operator/api/v1" apiv2 "github.com/wandb/operator/api/v2" - externalobjectstore "github.com/wandb/operator/internal/controller/infra/external/objectstore" + "github.com/wandb/operator/internal/controller/infra/objectstore" ) // migrateLegacyAnnotations drains `legacy.operator.wandb.com/*-pending` @@ -296,7 +296,7 @@ func deriveBucketAddressing(provider, endpoint string, query url.Values) (forceP if provider != "" && provider != "s3" && provider != "cw" { return "", "" } - fps := provider != "cw" && externalobjectstore.RequiresPathStyle(endpoint) + fps := provider != "cw" && objectstore.RequiresPathStyle(endpoint) if v, err := strconv.ParseBool(query.Get("forcePathStyle")); err == nil { fps = v } From fdd29ba46e92c0cba3f1339d452d67900b5e7228 Mon Sep 17 00:00:00 2001 From: wnevis-cmyk <wnevis@coreweave.com> Date: Mon, 27 Jul 2026 15:48:45 -0700 Subject: [PATCH 43/49] fix: Map external ClickHouse during v1->v2 conversion (#299) Co-authored-by: Cursor <cursoragent@cursor.com> --- api/v1/weightsandbiases_conversion_mapping.go | 109 +++++++++- api/v1/weightsandbiases_conversion_test.go | 202 ++++++++++++++++++ .../controller/reconciler/migrate_legacy.go | 65 +++++- .../reconciler/migrate_legacy_test.go | 124 +++++++++++ 4 files changed, 490 insertions(+), 10 deletions(-) diff --git a/api/v1/weightsandbiases_conversion_mapping.go b/api/v1/weightsandbiases_conversion_mapping.go index db9e64dd..d1c94b76 100644 --- a/api/v1/weightsandbiases_conversion_mapping.go +++ b/api/v1/weightsandbiases_conversion_mapping.go @@ -33,20 +33,22 @@ import ( // Default Secret keys used by v1's legacy ref blocks when only the Secret // name was specified. const ( - defaultMySQLPasswordSecretKey = "MYSQL_PASSWORD" - defaultRedisPasswordSecretKey = "REDIS_PASSWORD" - defaultOIDCClientSecretKey = "OIDC_SECRET" - defaultBucketAccessKeyName = "ACCESS_KEY" - defaultBucketSecretKeyName = "SECRET_KEY" + defaultMySQLPasswordSecretKey = "MYSQL_PASSWORD" + defaultRedisPasswordSecretKey = "REDIS_PASSWORD" + defaultOIDCClientSecretKey = "OIDC_SECRET" + defaultBucketAccessKeyName = "ACCESS_KEY" + defaultBucketSecretKeyName = "SECRET_KEY" + defaultClickHousePasswordSecretKey = "CLICKHOUSE_PASSWORD" ) // Annotations carrying v1 literals the reconciler materializes into Secrets // post-conversion (the webhook is stateless and can't create them itself). const ( - OIDCPendingAnnotation = "legacy.operator.wandb.com/oidc-pending" - MySQLPendingAnnotation = "legacy.operator.wandb.com/mysql-pending" - RedisPendingAnnotation = "legacy.operator.wandb.com/redis-pending" - BucketPendingAnnotation = "legacy.operator.wandb.com/bucket-pending" + OIDCPendingAnnotation = "legacy.operator.wandb.com/oidc-pending" + MySQLPendingAnnotation = "legacy.operator.wandb.com/mysql-pending" + RedisPendingAnnotation = "legacy.operator.wandb.com/redis-pending" + BucketPendingAnnotation = "legacy.operator.wandb.com/bucket-pending" + ClickHousePendingAnnotation = "legacy.operator.wandb.com/clickhouse-pending" ) var validSizes = map[string]appsv2.Size{ @@ -124,6 +126,9 @@ func applyGlobalMappings(globalMap map[string]interface{}, dst *appsv2.WeightsAn if err := mapBucket(globalMap, dst); err != nil { return err } + if err := mapClickHouse(globalMap, dst); err != nil { + return err + } return nil } @@ -517,6 +522,92 @@ func mapMySQL(globalMap map[string]interface{}, dst *appsv2.WeightsAndBiases) er return nil } +// clickHouseFields maps each v1 global.clickhouse.<key> to a +// *ClickHouseConnection setter (v1 `port` is the HTTP interface). +var clickHouseFields = []struct { + v1Key string + setRef func(*appsv2.ClickHouseConnection, corev1.SecretKeySelector) +}{ + {"host", func(c *appsv2.ClickHouseConnection, s corev1.SecretKeySelector) { c.Host = s }}, + {"port", func(c *appsv2.ClickHouseConnection, s corev1.SecretKeySelector) { c.HTTPPort = s }}, + {"database", func(c *appsv2.ClickHouseConnection, s corev1.SecretKeySelector) { c.Database = s }}, + {"user", func(c *appsv2.ClickHouseConnection, s corev1.SecretKeySelector) { c.Username = s }}, + {"password", func(c *appsv2.ClickHouseConnection, s corev1.SecretKeySelector) { c.Password = s }}, +} + +// mapClickHouse routes v1 global.clickhouse to externalClickhouse (like +// mapMySQL); external is asserted only when a connection field is present. +func mapClickHouse(globalMap map[string]interface{}, dst *appsv2.WeightsAndBiases) error { + chMap, found, err := unstructured.NestedMap(globalMap, "clickhouse") + if err != nil { + return fmt.Errorf("spec.values.global.clickhouse: %w", err) + } + if !found || len(chMap) == 0 { + return nil + } + + conn := &appsv2.ClickHouseConnection{} + remaining := map[string]string{} + sawField := false + + for _, f := range clickHouseFields { + raw, ok := chMap[f.v1Key] + if !ok { + continue + } + ref, literal, classifyErr := classifyValueFromOrLiteral(raw) + if classifyErr != nil { + return fmt.Errorf("spec.values.global.clickhouse.%s: %w", f.v1Key, classifyErr) + } + switch { + case ref != nil: + f.setRef(conn, *ref) + sawField = true + case literal != "": + remaining[f.v1Key] = literal + sawField = true + } + } + + if ps, ok, err := unstructured.NestedMap(chMap, "passwordSecret"); err != nil { + return fmt.Errorf("spec.values.global.clickhouse.passwordSecret: %w", err) + } else if ok { + name, _, err := unstructured.NestedString(ps, "name") + if err != nil { + return fmt.Errorf("spec.values.global.clickhouse.passwordSecret.name: %w", err) + } + alreadyHasPassword := conn.Password.Name != "" + if name != "" && !alreadyHasPassword { + key, _, err := unstructured.NestedString(ps, "passwordKey") + if err != nil { + return fmt.Errorf("spec.values.global.clickhouse.passwordSecret.passwordKey: %w", err) + } + if key == "" { + key = defaultClickHousePasswordSecretKey + } + conn.Password = corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: key, + } + delete(remaining, "password") + sawField = true + } + } + + if !sawField { + return nil + } + + dst.Spec.ClickHouse = map[string]appsv2.ClickHouseSpec{ + appsv2.DefaultInstanceName: {ExternalClickHouse: conn}, + } + + if len(remaining) > 0 { + return writeAnnotation(dst, ClickHousePendingAnnotation, remaining) + } + return nil +} + // redisFields maps each v1 global.redis.<key> to a *RedisConnection setter. var redisFields = []struct { v1Key string diff --git a/api/v1/weightsandbiases_conversion_test.go b/api/v1/weightsandbiases_conversion_test.go index 7d4ef679..014b614c 100644 --- a/api/v1/weightsandbiases_conversion_test.go +++ b/api/v1/weightsandbiases_conversion_test.go @@ -1801,3 +1801,205 @@ func TestConvertTo_StashedAnnotationsReflectCRNotActiveSpec(t *testing.T) { require.Equal(t, "http://wandb.from-cr", global["host"], "stashed annotation must preserve the CR's raw values for round-trip") } + +func TestConvertTo_ClickHouseValueFromRef(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "clickhouse": map[string]interface{}{ + "host": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "ch-settings", + "key": "endpoint", + }, + }, + }, + "password": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "ch-secret", + "key": "password", + }, + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + conn := dst.Spec.ClickHouse[appsv2.DefaultInstanceName].ExternalClickHouse + require.NotNil(t, conn) + require.Nil(t, dst.Spec.ClickHouse[appsv2.DefaultInstanceName].ManagedClickHouse, + "external clickhouse must not also be managed") + require.Equal(t, "ch-settings", conn.Host.Name) + require.Equal(t, "endpoint", conn.Host.Key) + require.Equal(t, "ch-secret", conn.Password.Name) + require.Equal(t, "password", conn.Password.Key) + + require.NotContains(t, dst.Annotations, ClickHousePendingAnnotation, + "no literals provided, so no annotation should be created") +} + +func TestConvertTo_ClickHouseLiterals(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "clickhouse": map[string]interface{}{ + "host": "clickhouse.example.com", + "port": int64(8123), + "database": "weave", + "user": "weave", + "password": "shh", + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + raw, ok := dst.Annotations[ClickHousePendingAnnotation] + require.True(t, ok, "expected clickhouse-pending annotation") + + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "clickhouse.example.com", decoded["host"]) + require.Equal(t, "8123", decoded["port"]) + require.Equal(t, "weave", decoded["database"]) + require.Equal(t, "weave", decoded["user"]) + require.Equal(t, "shh", decoded["password"]) + + require.NotNil(t, dst.Spec.ClickHouse[appsv2.DefaultInstanceName].ExternalClickHouse, + "externalClickhouse is always allocated; reconciler fills selectors from the annotation") + require.Empty(t, dst.Spec.ClickHouse[appsv2.DefaultInstanceName].ExternalClickHouse.Host.Name) +} + +func TestConvertTo_ClickHouseMixedLiteralsAndRefs(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "clickhouse": map[string]interface{}{ + "host": "clickhouse.example.com", + "port": int64(8123), + "password": map[string]interface{}{ + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "name": "ch-secret", + "key": "password", + }, + }, + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + conn := dst.Spec.ClickHouse[appsv2.DefaultInstanceName].ExternalClickHouse + require.NotNil(t, conn) + require.Equal(t, "ch-secret", conn.Password.Name) + require.Equal(t, "password", conn.Password.Key) + + raw := dst.Annotations[ClickHousePendingAnnotation] + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "clickhouse.example.com", decoded["host"]) + require.Equal(t, "8123", decoded["port"]) + require.NotContains(t, decoded, "password", "password came from a ref, not a literal") +} + +func TestConvertTo_ClickHouseLegacyPasswordSecret(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "clickhouse": map[string]interface{}{ + "host": "clickhouse.example.com", + "password": "shh", + "passwordSecret": map[string]interface{}{ + "name": "ch-creds", + "passwordKey": "CLICKHOUSE_PASSWORD", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + conn := dst.Spec.ClickHouse[appsv2.DefaultInstanceName].ExternalClickHouse + require.NotNil(t, conn) + require.Equal(t, "ch-creds", conn.Password.Name) + require.Equal(t, "CLICKHOUSE_PASSWORD", conn.Password.Key) + + raw := dst.Annotations[ClickHousePendingAnnotation] + var decoded map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(raw), &decoded)) + require.Equal(t, "clickhouse.example.com", decoded["host"]) + require.NotContains(t, decoded, "password", "literal password must not be stashed when passwordSecret took over") +} + +func TestConvertTo_ClickHousePasswordSecretDefaultKey(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "clickhouse": map[string]interface{}{ + "passwordSecret": map[string]interface{}{ + "name": "ch-creds", + }, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + conn := dst.Spec.ClickHouse[appsv2.DefaultInstanceName].ExternalClickHouse + require.NotNil(t, conn) + require.Equal(t, "ch-creds", conn.Password.Name) + require.Equal(t, "CLICKHOUSE_PASSWORD", conn.Password.Key) +} + +// TestConvertTo_ClickHousePasswordSecretMalformed: a non-string name must +// surface an error instead of being silently skipped. +func TestConvertTo_ClickHousePasswordSecretMalformed(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "clickhouse": map[string]interface{}{ + "passwordSecret": map[string]interface{}{ + "name": float64(123), + }, + }, + }, + }) + err := src.ConvertTo(dst) + require.Error(t, err) + require.Contains(t, err.Error(), "clickhouse.passwordSecret.name") +} + +// TestConvertTo_NoClickHouseLeavesEmpty: no clickhouse block means conversion +// leaves it empty for the defaulter to manage. +func TestConvertTo_NoClickHouseLeavesEmpty(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "host": "http://wandb.example.com", + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.Empty(t, dst.Spec.ClickHouse, + "no global.clickhouse means conversion leaves ClickHouse empty for the defaulter to manage") + require.NotContains(t, dst.Annotations, ClickHousePendingAnnotation) +} + +// TestConvertTo_ClickHouseOnlyNonConnectionKeys: keys like replicated/install +// must not be misread as an external connection. +func TestConvertTo_ClickHouseOnlyNonConnectionKeys(t *testing.T) { + dst := &appsv2.WeightsAndBiases{} + src := newV1(map[string]interface{}{ + "global": map[string]interface{}{ + "clickhouse": map[string]interface{}{ + "replicated": true, + }, + }, + }) + require.NoError(t, src.ConvertTo(dst)) + + require.Empty(t, dst.Spec.ClickHouse, + "only non-connection keys must not assert an external clickhouse") + require.NotContains(t, dst.Annotations, ClickHousePendingAnnotation) +} diff --git a/internal/controller/reconciler/migrate_legacy.go b/internal/controller/reconciler/migrate_legacy.go index 5b4fc865..ec667cbb 100644 --- a/internal/controller/reconciler/migrate_legacy.go +++ b/internal/controller/reconciler/migrate_legacy.go @@ -59,7 +59,11 @@ func migrateLegacyAnnotations( if err != nil { return ctrl.Result{}, err } - if !mysqlChanged && !redisChanged && !bucketChanged && !oidcChanged { + clickHouseChanged, err := migrateLegacyClickHouse(ctx, c, wandb) + if err != nil { + return ctrl.Result{}, err + } + if !mysqlChanged && !redisChanged && !bucketChanged && !oidcChanged && !clickHouseChanged { return ctrl.Result{}, nil } @@ -190,6 +194,65 @@ func migrateLegacyRedis( return true, nil } +// legacyClickHousePayload is the literal-string subset the webhook couldn't +// turn into typed selectors. Port is `any` to accept JSON number or string. +type legacyClickHousePayload struct { + Host string `json:"host,omitempty"` + Port any `json:"port,omitempty"` + Database string `json:"database,omitempty"` + User string `json:"user,omitempty"` + Password string `json:"password,omitempty"` +} + +// migrateLegacyClickHouse drains the clickhouse-pending annotation into a +// Secret + externalClickhouse selectors (v1 `port` fills HTTPPort). +func migrateLegacyClickHouse( + ctx context.Context, + c ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, +) (bool, error) { + raw, ok := wandb.Annotations[apiv1.ClickHousePendingAnnotation] + if !ok { + return false, nil + } + + dec := json.NewDecoder(strings.NewReader(raw)) + dec.UseNumber() + var payload legacyClickHousePayload + if err := dec.Decode(&payload); err != nil { + return false, fmt.Errorf("decode %s: %w", apiv1.ClickHousePendingAnnotation, err) + } + + secretName := fmt.Sprintf("%s-clickhouse-converted", wandb.Name) + conn := wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ExternalClickHouse + if conn == nil { + conn = &apiv2.ClickHouseConnection{} + } + + data := map[string][]byte{} + fill := func(target *corev1.SecretKeySelector, dataKey, value string) { + if target.Name != "" || value == "" { + return + } + data[dataKey] = []byte(value) + *target = secretSelector(secretName, dataKey) + } + + fill(&conn.Host, "host", payload.Host) + fill(&conn.HTTPPort, "httpPort", normalizePort(payload.Port)) + fill(&conn.Database, "database", payload.Database) + fill(&conn.Username, "username", payload.User) + fill(&conn.Password, "password", payload.Password) + + if err := materializeConvertedSecret(ctx, c, wandb, secretName, data); err != nil { + return false, err + } + + setExternalInstance(&wandb.Spec.ClickHouse, func(s *apiv2.ClickHouseSpec) { s.ExternalClickHouse = conn }) + delete(wandb.Annotations, apiv1.ClickHousePendingAnnotation) + return true, nil +} + // legacyBucketPayload is the flat literal subset from the webhook's // bucket+defaultBucket merge. kmsKey has no v2 home; ignored. type legacyBucketPayload struct { diff --git a/internal/controller/reconciler/migrate_legacy_test.go b/internal/controller/reconciler/migrate_legacy_test.go index 76ff18a2..07aedfa5 100644 --- a/internal/controller/reconciler/migrate_legacy_test.go +++ b/internal/controller/reconciler/migrate_legacy_test.go @@ -88,6 +88,13 @@ func getOIDCConvertedSecret(t *testing.T, c ctrlClient.Client) (*corev1.Secret, return &secret, err } +func getClickHouseConvertedSecret(t *testing.T, c ctrlClient.Client) (*corev1.Secret, error) { + t.Helper() + var secret corev1.Secret + err := c.Get(context.Background(), types.NamespacedName{Name: "wandb-clickhouse-converted", Namespace: "default"}, &secret) + return &secret, err +} + func TestMigrateLegacyAnnotations_NoAnnotation(t *testing.T) { client, wandb := newMigrationFixture(t, nil, nil) res, err := migrateLegacyAnnotations(context.Background(), client, wandb) @@ -717,3 +724,120 @@ func TestMigrateLegacyOIDC_MalformedJSON(t *testing.T) { require.Error(t, err) require.Contains(t, wandb.Annotations, apiv1.OIDCPendingAnnotation) } + +func TestMigrateLegacyClickHouse_FullLiteralPayload(t *testing.T) { + payload := `{"host":"clickhouse.example.com","port":8123,"database":"weave","user":"weave","password":"shh"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.ClickHousePendingAnnotation: payload, + }, nil) + + res, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + require.NotZero(t, res.RequeueAfter) + + secret, err := getClickHouseConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, corev1.SecretTypeOpaque, secret.Type) + require.Equal(t, []byte("clickhouse.example.com"), secret.Data["host"]) + require.Equal(t, []byte("8123"), secret.Data["httpPort"]) + require.Equal(t, []byte("weave"), secret.Data["database"]) + require.Equal(t, []byte("weave"), secret.Data["username"]) + require.Equal(t, []byte("shh"), secret.Data["password"]) + + var fresh apiv2.WeightsAndBiases + require.NoError(t, client.Get(context.Background(), types.NamespacedName{Name: "wandb", Namespace: "default"}, &fresh)) + require.NotContains(t, fresh.Annotations, apiv1.ClickHousePendingAnnotation) + conn := fresh.Spec.ClickHouse[apiv2.DefaultInstanceName].ExternalClickHouse + require.NotNil(t, conn) + require.Nil(t, fresh.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse) + require.Equal(t, "wandb-clickhouse-converted", conn.Host.Name) + require.Equal(t, "host", conn.Host.Key) + require.Equal(t, "httpPort", conn.HTTPPort.Key) + require.Equal(t, "database", conn.Database.Key) + require.Equal(t, "username", conn.Username.Key) + require.Equal(t, "password", conn.Password.Key) + require.Empty(t, conn.TCPPort.Name) + require.Empty(t, conn.URL.Name) +} + +func TestMigrateLegacyClickHouse_PartialPayload(t *testing.T) { + payload := `{"host":"clickhouse.example.com","password":"shh"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.ClickHousePendingAnnotation: payload, + }, nil) + + res, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + require.NotZero(t, res.RequeueAfter) + + secret, err := getClickHouseConvertedSecret(t, client) + require.NoError(t, err) + require.Contains(t, secret.Data, "host") + require.Contains(t, secret.Data, "password") + require.NotContains(t, secret.Data, "httpPort") + require.NotContains(t, secret.Data, "database") + require.NotContains(t, secret.Data, "username") + + conn := wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ExternalClickHouse + require.NotNil(t, conn) + require.Equal(t, "host", conn.Host.Key) + require.Equal(t, "password", conn.Password.Key) + require.Empty(t, conn.HTTPPort.Name) + require.Empty(t, conn.Database.Name) + require.Empty(t, conn.Username.Name) +} + +// TestMigrateLegacyClickHouse_PreSetFieldsAreRespected: preset selectors are +// not overwritten by the annotation drain. +func TestMigrateLegacyClickHouse_PreSetFieldsAreRespected(t *testing.T) { + payload := `{"host":"clickhouse.example.com","password":"shh"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.ClickHousePendingAnnotation: payload, + }, func(w *apiv2.WeightsAndBiases) { + w.Spec.ClickHouse = map[string]apiv2.ClickHouseSpec{ + apiv2.DefaultInstanceName: { + ExternalClickHouse: &apiv2.ClickHouseConnection{ + Password: corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "preset-ch"}, + Key: "PRESET", + }, + }, + }, + } + }) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getClickHouseConvertedSecret(t, client) + require.NoError(t, err) + require.Contains(t, secret.Data, "host") + require.NotContains(t, secret.Data, "password", "preset password selector must be respected") + + conn := wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ExternalClickHouse + require.Equal(t, "preset-ch", conn.Password.Name) + require.Equal(t, "PRESET", conn.Password.Key) +} + +func TestMigrateLegacyClickHouse_PortStringified(t *testing.T) { + payload := `{"port":"8123"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.ClickHousePendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getClickHouseConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("8123"), secret.Data["httpPort"]) +} + +func TestMigrateLegacyClickHouse_MalformedJSON(t *testing.T) { + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.ClickHousePendingAnnotation: "{not json", + }, nil) + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.Error(t, err) + require.Contains(t, wandb.Annotations, apiv1.ClickHousePendingAnnotation) +} From 00700c9bb52901ace8356d826834f7882f6dbc0b Mon Sep 17 00:00:00 2001 From: Jagat Thakkar <32109558+jthakkar04@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:46:23 -0500 Subject: [PATCH 44/49] fix: Parse Bucket Endpoint (#301) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- .../controller/infra/objectstore/endpoint.go | 41 +++++ .../infra/objectstore/endpoint_test.go | 40 ++++ .../controller/reconciler/migrate_legacy.go | 22 +-- .../reconciler/migrate_legacy_test.go | 172 +++++++++++++++--- 4 files changed, 234 insertions(+), 41 deletions(-) diff --git a/internal/controller/infra/objectstore/endpoint.go b/internal/controller/infra/objectstore/endpoint.go index c54cd612..b4aa71ff 100644 --- a/internal/controller/infra/objectstore/endpoint.go +++ b/internal/controller/infra/objectstore/endpoint.go @@ -2,6 +2,8 @@ package objectstore import ( "fmt" + "net" + "strconv" "strings" ) @@ -49,3 +51,42 @@ func AzureBlobURI(account, container, prefix string) string { } return uri } + +// ParseLegacyBucket splits a v1 bucket.name/path into endpoint, port, bucket and prefix. +func ParseLegacyBucket(provider, name, path string) (endpoint, port, bucket, prefix string) { + prefix = strings.Trim(path, "/") + + // host[:port]/bucket + if slash := strings.IndexByte(name, '/'); slash >= 0 { + host, bkt := name[:slash], name[slash+1:] + if colon := strings.IndexByte(host, ':'); colon >= 0 { + return host[:colon], host[colon+1:], bkt, prefix + } + return host, "", bkt, prefix + } + + // host:port name, bucket in path + if host, hostPort := splitHostPort(name); host != "" && prefix != "" && S3Compatible(provider) { + bkt, rest, _ := strings.Cut(prefix, "/") + return host, hostPort, bkt, rest + } + + return "", "", name, prefix +} + +// splitHostPort parses "host:port", returning "","" if the port is missing or invalid. +func splitHostPort(name string) (host, port string) { + host, port, err := net.SplitHostPort(name) + if err != nil || host == "" { + return "", "" + } + if n, err := strconv.ParseUint(port, 10, 16); err != nil || n == 0 { + return "", "" + } + return host, port +} + +// S3Compatible reports whether the provider uses an S3-style endpoint. +func S3Compatible(provider string) bool { + return provider == "" || provider == "s3" || provider == "cw" +} diff --git a/internal/controller/infra/objectstore/endpoint_test.go b/internal/controller/infra/objectstore/endpoint_test.go index c7c5fb84..5a0eee2e 100644 --- a/internal/controller/infra/objectstore/endpoint_test.go +++ b/internal/controller/infra/objectstore/endpoint_test.go @@ -60,3 +60,43 @@ func TestAzureBlobURI(t *testing.T) { "https://acct.blob.core.windows.net/container/some/prefix", AzureBlobURI("acct", "container", "some/prefix")) } + +func TestS3Compatible(t *testing.T) { + for _, p := range []string{"", "s3", "cw"} { + require.True(t, S3Compatible(p), p) + } + for _, p := range []string{"gcs", "az", "azure"} { + require.False(t, S3Compatible(p), p) + } +} + +func TestParseLegacyBucket(t *testing.T) { + cases := []struct { + desc string + provider, name, path string + endpoint, port, bkt, prefix string + }{ + {"empty", "", "", "", "", "", "", ""}, + {"bare bucket", "s3", "my-bucket", "", "", "", "my-bucket", ""}, + {"bare bucket with prefix", "s3", "my-bucket", "prefix", "", "", "my-bucket", "prefix"}, + {"embedded host", "", "minio.example.com/wandb", "", "minio.example.com", "", "wandb", ""}, + {"embedded host:port", "", "minio.example.com:9000/wandb", "", "minio.example.com", "9000", "wandb", ""}, + {"embedded short host:port", "", "minio:9000/wandb", "", "minio", "9000", "wandb", ""}, + {"embedded fqdn host:port", "", "minio.minio.svc.cluster.local:9000/bucket", "", "minio.minio.svc.cluster.local", "9000", "bucket", ""}, + {"host:port name, bucket in path", "s3", "minio.minio.svc.cluster.local:9000", "lsahu-minio-bucket", "minio.minio.svc.cluster.local", "9000", "lsahu-minio-bucket", ""}, + {"host:port name, bucket + prefix in path", "s3", "minio:9000", "/bucket/team/project/", "minio", "9000", "bucket", "team/project"}, + {"host:port name, no provider", "", "minio:9000", "bucket", "minio", "9000", "bucket", ""}, + {"aws bucket with path is not endpoint", "s3", "my-aws-bucket", "prefix", "", "", "my-aws-bucket", "prefix"}, + {"host:port name but non-s3 provider", "gcs", "foo:9000", "bucket", "", "", "foo:9000", "bucket"}, + {"ipv6 host:port name", "s3", "[fd00::1]:9000", "bucket", "fd00::1", "9000", "bucket", ""}, + } + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + e, p, b, pre := ParseLegacyBucket(tc.provider, tc.name, tc.path) + require.Equal(t, tc.endpoint, e) + require.Equal(t, tc.port, p) + require.Equal(t, tc.bkt, b) + require.Equal(t, tc.prefix, pre) + }) + } +} diff --git a/internal/controller/reconciler/migrate_legacy.go b/internal/controller/reconciler/migrate_legacy.go index ec667cbb..c3557bd5 100644 --- a/internal/controller/reconciler/migrate_legacy.go +++ b/internal/controller/reconciler/migrate_legacy.go @@ -288,7 +288,7 @@ func migrateLegacyBucket( } name, path, query := splitBucketQuery(payload.Name, payload.Path) - endpoint, port, bucket := parseBucketName(name) + endpoint, port, bucket, path := objectstore.ParseLegacyBucket(payload.Provider, name, path) // Query param beats the region field, matching gorilla's precedence. region := payload.Region if v := query.Get("region"); v != "" { @@ -308,7 +308,7 @@ func migrateLegacyBucket( fill(&conn.Endpoint, "endpoint", endpoint) fill(&conn.Port, "port", port) fill(&conn.Bucket, "bucket", bucket) - fill(&conn.Path, "path", strings.Trim(path, "/")) + fill(&conn.Path, "path", path) fill(&conn.Region, "region", region) fill(&conn.AccessKey, "accessKey", payload.AccessKey) fill(&conn.SecretKey, "secretKey", payload.SecretKey) @@ -356,7 +356,7 @@ func splitBucketQuery(name, path string) (cleanName, cleanPath string, q url.Val // explicit ?forcePathStyle=/?tls= win, else any embedded endpoint means path-style over // http (prefixes belong in bucket.path, so a host in bucket.name is always an endpoint). func deriveBucketAddressing(provider, endpoint string, query url.Values) (forcePathStyle, tlsEnabled string) { - if provider != "" && provider != "s3" && provider != "cw" { + if !objectstore.S3Compatible(provider) { return "", "" } fps := provider != "cw" && objectstore.RequiresPathStyle(endpoint) @@ -375,22 +375,6 @@ func deriveBucketAddressing(provider, endpoint string, query url.Values) (forceP return forcePathStyle, strconv.FormatBool(tls) } -// parseBucketName splits v1's bucket.name. A "/" indicates the embedded -// "host[:port]/bucket" form (S3 bucket names can't contain "/"); otherwise -// the whole string is the bucket name. -func parseBucketName(name string) (endpoint, port, bucket string) { - if name == "" || !strings.Contains(name, "/") { - return "", "", name - } - slash := strings.IndexByte(name, '/') - host := name[:slash] - bucket = name[slash+1:] - if colon := strings.IndexByte(host, ':'); colon >= 0 { - return host[:colon], host[colon+1:], bucket - } - return host, "", bucket -} - // legacyOIDCPayload is the literal-string subset the webhook couldn't turn // into typed selectors. type legacyOIDCPayload struct { diff --git a/internal/controller/reconciler/migrate_legacy_test.go b/internal/controller/reconciler/migrate_legacy_test.go index 07aedfa5..6a16d569 100644 --- a/internal/controller/reconciler/migrate_legacy_test.go +++ b/internal/controller/reconciler/migrate_legacy_test.go @@ -409,28 +409,6 @@ func TestMigrateLegacyMySQL_PortStringValueAccepted(t *testing.T) { require.Equal(t, []byte("3308"), secret.Data["port"]) } -func TestParseBucketName(t *testing.T) { - cases := []struct { - name string - endpoint, port, bkt string - }{ - {"", "", "", ""}, - {"my-bucket", "", "", "my-bucket"}, - {"minio.example.com/wandb", "minio.example.com", "", "wandb"}, - {"minio.example.com:9000/wandb", "minio.example.com", "9000", "wandb"}, - {"minio:9000/wandb", "minio", "9000", "wandb"}, - {"minio.minio.svc.cluster.local:9000/bucket", "minio.minio.svc.cluster.local", "9000", "bucket"}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - e, p, b := parseBucketName(tc.name) - require.Equal(t, tc.endpoint, e) - require.Equal(t, tc.port, p) - require.Equal(t, tc.bkt, b) - }) - } -} - func TestMigrateLegacyBucket_BareBucketName(t *testing.T) { payload := `{"name":"my-bucket","region":"us-east-1","accessKey":"AKIA","secretKey":"shh"}` client, wandb := newMigrationFixture(t, map[string]string{ @@ -488,6 +466,156 @@ func TestMigrateLegacyBucket_EmbeddedEndpoint(t *testing.T) { require.Equal(t, "tlsEnabled", conn.TlsEnabled.Key) } +func TestMigrateLegacyBucket_HostPortEndpointWithBucketInPath(t *testing.T) { + payload := `{ + "provider": "s3", + "name": "minio.minio.svc.cluster.local:9000", + "path": "lsahu-minio-bucket", + "region": "us-east-1" +}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, func(w *apiv2.WeightsAndBiases) { + w.Spec.ObjectStore = map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: { + ExternalObjectStore: &apiv2.ObjectStoreConnection{ + AccessKey: secretSelector("wandb-minio", "ACCESS_KEY"), + SecretKey: secretSelector("wandb-minio", "SECRET_KEY"), + }, + }, + } + }) + + res, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + require.NotZero(t, res.RequeueAfter) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("minio.minio.svc.cluster.local"), secret.Data["endpoint"]) + require.Equal(t, []byte("9000"), secret.Data["port"]) + require.Equal(t, []byte("lsahu-minio-bucket"), secret.Data["bucket"]) + require.Equal(t, []byte("us-east-1"), secret.Data["region"]) + require.Equal(t, []byte("true"), secret.Data["forcePathStyle"]) + require.Equal(t, []byte("false"), secret.Data["tlsEnabled"]) + require.NotContains(t, secret.Data, "path") + require.NotContains(t, secret.Data, "accessKey") + require.NotContains(t, secret.Data, "secretKey") + + var fresh apiv2.WeightsAndBiases + require.NoError(t, client.Get(context.Background(), types.NamespacedName{Name: "wandb", Namespace: "default"}, &fresh)) + require.NotContains(t, fresh.Annotations, apiv1.BucketPendingAnnotation) + + conn := fresh.Spec.ObjectStore[apiv2.DefaultInstanceName].ExternalObjectStore + require.Equal(t, secretSelector("wandb-bucket-converted", "endpoint"), conn.Endpoint) + require.Equal(t, secretSelector("wandb-bucket-converted", "port"), conn.Port) + require.Equal(t, secretSelector("wandb-bucket-converted", "bucket"), conn.Bucket) + require.Equal(t, secretSelector("wandb-bucket-converted", "region"), conn.Region) + require.Equal(t, secretSelector("wandb-bucket-converted", "forcePathStyle"), conn.ForcePathStyle) + require.Equal(t, secretSelector("wandb-bucket-converted", "tlsEnabled"), conn.TlsEnabled) + require.Empty(t, conn.Path.Name) + require.Equal(t, secretSelector("wandb-minio", "ACCESS_KEY"), conn.AccessKey) + require.Equal(t, secretSelector("wandb-minio", "SECRET_KEY"), conn.SecretKey) +} + +func TestMigrateLegacyBucket_HostPortEndpointWithBucketAndPrefixInPath(t *testing.T) { + payload := `{ + "provider": "s3", + "name": "minio.minio.svc.cluster.local:9000", + "path": "/lsahu-minio-bucket/team/project/", + "region": "us-east-1" +}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("minio.minio.svc.cluster.local"), secret.Data["endpoint"]) + require.Equal(t, []byte("9000"), secret.Data["port"]) + require.Equal(t, []byte("lsahu-minio-bucket"), secret.Data["bucket"]) + require.Equal(t, []byte("team/project"), secret.Data["path"]) + + conn := wandb.Spec.ObjectStore[apiv2.DefaultInstanceName].ExternalObjectStore + require.Equal(t, "bucket", conn.Bucket.Key) + require.Equal(t, "path", conn.Path.Key) +} + +func TestMigrateLegacyBucket_AWSBucketWithPathIsNotEndpoint(t *testing.T) { + payload := `{"provider":"s3","name":"my-aws-bucket","path":"prefix","region":"us-east-1"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("my-aws-bucket"), secret.Data["bucket"]) + require.Equal(t, []byte("prefix"), secret.Data["path"]) + require.NotContains(t, secret.Data, "endpoint") + require.NotContains(t, secret.Data, "port") + require.Equal(t, []byte("false"), secret.Data["forcePathStyle"]) + require.NotContains(t, secret.Data, "tlsEnabled") +} + +func TestMigrateLegacyBucket_HostPortEndpointNoProvider(t *testing.T) { + payload := `{"name":"minio.minio.svc:9000","path":"wandb-bucket","region":"us-east-1"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("minio.minio.svc"), secret.Data["endpoint"], "host:port name is an endpoint even without a provider") + require.Equal(t, []byte("9000"), secret.Data["port"]) + require.Equal(t, []byte("wandb-bucket"), secret.Data["bucket"]) + require.Equal(t, []byte("true"), secret.Data["forcePathStyle"]) + require.Equal(t, []byte("false"), secret.Data["tlsEnabled"]) + require.NotContains(t, secret.Data, "path") +} + +func TestMigrateLegacyBucket_HostPortEndpointQueryInPath(t *testing.T) { + payload := `{"provider":"s3","name":"minio.example.com:9000","path":"wandb-bucket/team?tls=true"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("minio.example.com"), secret.Data["endpoint"]) + require.Equal(t, []byte("9000"), secret.Data["port"]) + require.Equal(t, []byte("wandb-bucket"), secret.Data["bucket"]) + require.Equal(t, []byte("team"), secret.Data["path"], "query is stripped, the prefix survives") + require.Equal(t, []byte("true"), secret.Data["tlsEnabled"], "?tls= on the path still wins") +} + +func TestMigrateLegacyBucket_HostPortEndpointIPv6(t *testing.T) { + payload := `{"provider":"s3","name":"[fd00::1]:9000","path":"wandb-bucket"}` + client, wandb := newMigrationFixture(t, map[string]string{ + apiv1.BucketPendingAnnotation: payload, + }, nil) + + _, err := migrateLegacyAnnotations(context.Background(), client, wandb) + require.NoError(t, err) + + secret, err := getBucketConvertedSecret(t, client) + require.NoError(t, err) + require.Equal(t, []byte("fd00::1"), secret.Data["endpoint"]) + require.Equal(t, []byte("9000"), secret.Data["port"]) + require.Equal(t, []byte("wandb-bucket"), secret.Data["bucket"]) +} + func TestMigrateLegacyBucket_QueryParamOverrides(t *testing.T) { payload := `{"provider":"s3","name":"minio.example.com:9000/wandb","region":"us-east-1","path":"prefix?tls=true&forcePathStyle=false®ion=eu-west-1"}` client, wandb := newMigrationFixture(t, map[string]string{ From c1106d202689539fe355fbff15b63efd6c26e353 Mon Sep 17 00:00:00 2001 From: Daniel Panzella <daniel.panzella@wandb.com> Date: Tue, 28 Jul 2026 13:38:34 -0700 Subject: [PATCH 45/49] feat: Support custom CA's in operator v2 (#303) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> --- README.md | 38 +++++++++++++++++ deploy/operator/templates/_helpers.tpl | 42 +++++++++++++++++++ .../operator/templates/operator-ca-certs.yaml | 25 +++++++++++ deploy/operator/values.yaml | 21 ++++++++++ 4 files changed, 126 insertions(+) create mode 100644 deploy/operator/templates/operator-ca-certs.yaml diff --git a/README.md b/README.md index eedb9385..ab78ed55 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,44 @@ The operator reconciles the resource, brings up the requested backing services, and rolls out the W&B application. See [`deploy/operator/values.yaml`](deploy/operator/values.yaml) for the available chart options and which component operators are enabled. +### Custom CA certificates (air-gapped / private registry) + +When the operator must reach a private OCI registry served with a self-signed or +corporate CA — for example to pull the server manifest in an air-gapped install — +provide the CA at install time via `wandb-operator.caCerts`. The certs are mounted +into the operator pod and added to `SSL_CERT_DIR` additively, so the system trust +bundle (and any public roots) is preserved. + +Provide the CA one of three ways: + +```yaml +# values.yaml — pick ONE source +wandb-operator: + caCerts: + # 1. inline PEM blocks (synthesized into a Secret by the chart) + certs: + - | + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- + # 2. or an existing Secret whose keys are PEM certs + existingSecret: my-registry-ca + # 3. or an existing ConfigMap whose keys are PEM certs + existingConfigMap: my-registry-ca +``` + +```bash +# e.g. mount a CA file straight from disk +helm install wandb-operator oci://.../charts/operator \ + --namespace wandb-operators --create-namespace \ + --set-file 'wandb-operator.caCerts.certs[0]=./registry-ca.crt' +``` + +This trusts the CA for the operator's own egress (the server-manifest pull). To +trust a CA on the W&B **application** workloads instead, use +`spec.global.customCACerts` / `spec.global.caCertsConfigMap` on the +`WeightsAndBiases` resource. + ## Documentation - [Configuration API](docs/config-api.md) diff --git a/deploy/operator/templates/_helpers.tpl b/deploy/operator/templates/_helpers.tpl index 8c1e63e5..b39d2ee8 100644 --- a/deploy/operator/templates/_helpers.tpl +++ b/deploy/operator/templates/_helpers.tpl @@ -37,3 +37,45 @@ inside operator-crds/crds.yaml: honor an explicit override on {{- define "wandb-operator.serviceName" -}} {{ include "wandb-base.serviceName" (dict "Release" (dict "Name" .Release.Name) "Chart" (dict "Name" "wandb-operator") "Values" (dict "nameOverride" (dig "wandb-operator" "nameOverride" "" .Values.AsMap) "service" (dict "name" (dig "wandb-operator" "service" "name" "" .Values.AsMap)))) }} {{- end }} + +{{/* +Operator custom-CA trust (wandb-operator.caCerts). Referenced from the +wandb-operator subchart values as volumesTpls / volumeMountsTpls / envTpls +strings, so they render in the subchart context where .Values is the +wandb-operator values. Each is inert unless a CA source is configured. +*/}} +{{- define "wandb-operator.caCertsActive" -}} +{{- $ca := .Values.caCerts | default dict -}} +{{- if or $ca.certs $ca.existingSecret $ca.existingConfigMap -}}true{{- end -}} +{{- end -}} + +{{- define "wandb-operator.caCertsVolume" -}} +{{- $ca := .Values.caCerts | default dict -}} +{{- if include "wandb-operator.caCertsActive" . -}} +- name: wandb-operator-ca-certs + {{- if $ca.existingConfigMap }} + configMap: + name: {{ $ca.existingConfigMap }} + {{- else }} + secret: + secretName: {{ $ca.existingSecret | default (printf "%s-operator-ca-certs" .Release.Name) }} + {{- end }} +{{- end -}} +{{- end -}} + +{{- define "wandb-operator.caCertsVolumeMount" -}} +{{- $ca := .Values.caCerts | default dict -}} +{{- if include "wandb-operator.caCertsActive" . -}} +- name: wandb-operator-ca-certs + mountPath: {{ $ca.mountPath | default "/etc/wandb/ca-certs" }} + readOnly: true +{{- end -}} +{{- end -}} + +{{- define "wandb-operator.caCertsEnv" -}} +{{- $ca := .Values.caCerts | default dict -}} +{{- if include "wandb-operator.caCertsActive" . -}} +- name: SSL_CERT_DIR + value: "{{ $ca.mountPath | default "/etc/wandb/ca-certs" }}:/etc/ssl/certs:/etc/pki/tls/certs" +{{- end -}} +{{- end -}} diff --git a/deploy/operator/templates/operator-ca-certs.yaml b/deploy/operator/templates/operator-ca-certs.yaml new file mode 100644 index 00000000..35666852 --- /dev/null +++ b/deploy/operator/templates/operator-ca-certs.yaml @@ -0,0 +1,25 @@ +{{- /* +Synthesizes a Secret from inline wandb-operator.caCerts.certs (PEM blocks) so the +operator container can trust a private/self-signed registry CA. Skipped when the +user points caCerts at an existing Secret/ConfigMap instead. The name matches the +default secretName referenced by wandb-operator.volumesTpls in values.yaml. +*/ -}} +{{- $op := index .Values "wandb-operator" | default dict -}} +{{- $ca := $op.caCerts | default dict -}} +{{- if and $ca.certs (not $ca.existingSecret) (not $ca.existingConfigMap) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-operator-ca-certs + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/part-of: wandb +type: Opaque +stringData: + {{- range $i, $pem := $ca.certs }} + ca-{{ $i }}.crt: | + {{- $pem | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/operator/values.yaml b/deploy/operator/values.yaml index f6c1c1b9..754ca416 100644 --- a/deploy/operator/values.yaml +++ b/deploy/operator/values.yaml @@ -65,6 +65,27 @@ wandb-operator: - mountPath: /tmp/wandb-operator/serving-certs name: serving-certs readOnly: true + # Mounts the custom CA bundle (see caCerts below) into the operator + # container. Rendered only when caCerts has a source. + volumeMountsTpls: + - '{{ include "wandb-operator.caCertsVolumeMount" . }}' + + # Custom CA certificates trusted by the operator process. Required when the + # operator must pull the server manifest from a private/self-signed OCI + # registry (air-gapped installs). Activates when any source below is set; + # certs are added to SSL_CERT_DIR additively (Go still loads the ubi system + # bundle, so public roots are preserved). + caCerts: + # Inline PEM blocks; the parent chart synthesizes them into a Secret. + certs: [] + # Or reference an existing Secret / ConfigMap whose keys are PEM certs. + existingSecret: "" + existingConfigMap: "" + mountPath: /etc/wandb/ca-certs + volumesTpls: + - '{{ include "wandb-operator.caCertsVolume" . }}' + envTpls: + - '{{ include "wandb-operator.caCertsEnv" . }}' service: enabled: true From 81a45903b3847000fd7ca14815741dd05b277c6c Mon Sep 17 00:00:00 2001 From: Casey Abernathy <cabernathy@coreweave.com> Date: Tue, 28 Jul 2026 14:25:06 -0700 Subject: [PATCH 46/49] feat(operator): Create beta 3 release (#306) --- deploy/operator/Chart.yaml | 4 ++-- deploy/operator/values.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/operator/Chart.yaml b/deploy/operator/Chart.yaml index a38b8c3b..f4a538ca 100644 --- a/deploy/operator/Chart.yaml +++ b/deploy/operator/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: operator description: A Helm chart for Weights & Biases operator type: application -version: 2.0.0-beta.1 -appVersion: "2.0.0-beta.1" +version: 2.0.0-beta.3 +appVersion: "2.0.0-beta.3" maintainers: - name: wandb email: support@wandb.com diff --git a/deploy/operator/values.yaml b/deploy/operator/values.yaml index 754ca416..ca8039c1 100644 --- a/deploy/operator/values.yaml +++ b/deploy/operator/values.yaml @@ -15,7 +15,7 @@ wandb: wandb-operator: image: repository: us-docker.pkg.dev/wandb-production/public/wandb/operator - tag: 2.0.0-beta.1 + tag: 2.0.0-beta.3 containers: operator: command: From 860356c79cb9c22aaab600b7c3ec514e9dd27409 Mon Sep 17 00:00:00 2001 From: Jagat Thakkar <32109558+jthakkar04@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:55:27 -0500 Subject: [PATCH 47/49] chore: Updating WandB Version (#307) --- deploy/operator/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/operator/values.yaml b/deploy/operator/values.yaml index ca8039c1..61d72dd2 100644 --- a/deploy/operator/values.yaml +++ b/deploy/operator/values.yaml @@ -9,7 +9,7 @@ wandb: install: true size: small namespace: wandb - version: 0.82.2 + version: 0.83.1 internalServiceAuth: false wandb-operator: From 2ddcad093683a55fd4a37955a0fa9acf7f5feb34 Mon Sep 17 00:00:00 2001 From: Jagat Thakkar <32109558+jthakkar04@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:16:33 -0500 Subject: [PATCH 48/49] fix: Allowing a Release with duplication (#308) --- .github/workflows/release.yaml | 60 +++++++++++++++++----------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9b6e9f72..c706ad44 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -100,36 +100,36 @@ jobs: - name: Authorize Docker for Artifact Registry run: gcloud auth configure-docker us-docker.pkg.dev --quiet - - name: Reject existing production artifacts - env: - VERSION: ${{ steps.release.outputs.version }} - shell: bash - run: | - set -euo pipefail - - assert_absent() { - local artifact=$1 - local output - local status - - set +e - output="$(gcloud artifacts docker images describe "${artifact}" --format='value(image_summary.digest)' 2>&1)" - status=$? - set -e - - if [[ ${status} -eq 0 ]]; then - echo "Refusing to overwrite existing artifact ${artifact}" >&2 - exit 1 - fi - if ! grep -Eqi 'NOT_FOUND|not found' <<< "${output}"; then - echo "Could not safely determine whether ${artifact} exists:" >&2 - echo "${output}" >&2 - exit 1 - fi - } - - assert_absent "${IMAGE_REPOSITORY}:${VERSION}" - assert_absent "${CHART_REPOSITORY}/operator:${VERSION}" +# - name: Reject existing production artifacts +# env: +# VERSION: ${{ steps.release.outputs.version }} +# shell: bash +# run: | +# set -euo pipefail +# +# assert_absent() { +# local artifact=$1 +# local output +# local status +# +# set +e +# output="$(gcloud artifacts docker images describe "${artifact}" --format='value(image_summary.digest)' 2>&1)" +# status=$? +# set -e +# +# if [[ ${status} -eq 0 ]]; then +# echo "Refusing to overwrite existing artifact ${artifact}" >&2 +# exit 1 +# fi +# if ! grep -Eqi 'NOT_FOUND|not found' <<< "${output}"; then +# echo "Could not safely determine whether ${artifact} exists:" >&2 +# echo "${output}" >&2 +# exit 1 +# fi +# } +# +# assert_absent "${IMAGE_REPOSITORY}:${VERSION}" +# assert_absent "${CHART_REPOSITORY}/operator:${VERSION}" - name: Build and publish image env: From db6e8d92dce39ce9727bba2dab7216ab6d1da359 Mon Sep 17 00:00:00 2001 From: Collin Olander <colander@coreweave.com> Date: Mon, 3 Aug 2026 11:47:11 -0500 Subject: [PATCH 49/49] fix: Update-images (#310) --- .../0.83.0-clickhouse-keeper.1/manifest.yaml | 7 ++++- .../0.83.0-clickhouse-keeper.2/manifest.yaml | 7 ++++- .../clickhouse/altinity/keeper/spec.go | 12 +++++++- .../clickhouse/altinity/keeper/spec_test.go | 28 ++++++++++++++++--- .../clickhouse/altinity/keeper/values.go | 6 ++-- internal/controller/reconciler/clickhouse.go | 2 +- 6 files changed, 52 insertions(+), 10 deletions(-) diff --git a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml index 0769371c..9bbdaf76 100644 --- a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml +++ b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml @@ -26,7 +26,12 @@ clickhouse: tag: "25.8.16.10002.altinitystable" clickhouseKeeper: - default: {} + default: + images: + keeper: + registry: "docker.io" + repository: "altinity/clickhouse-keeper" + tag: "25.8.16.10002.altinitystable" generatedSecrets: - name: session-key diff --git a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml index 0769371c..9bbdaf76 100644 --- a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml +++ b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml @@ -26,7 +26,12 @@ clickhouse: tag: "25.8.16.10002.altinitystable" clickhouseKeeper: - default: {} + default: + images: + keeper: + registry: "docker.io" + repository: "altinity/clickhouse-keeper" + tag: "25.8.16.10002.altinitystable" generatedSecrets: - name: session-key diff --git a/internal/controller/infra/managed/clickhouse/altinity/keeper/spec.go b/internal/controller/infra/managed/clickhouse/altinity/keeper/spec.go index 812c757c..4eca4bfe 100644 --- a/internal/controller/infra/managed/clickhouse/altinity/keeper/spec.go +++ b/internal/controller/infra/managed/clickhouse/altinity/keeper/spec.go @@ -10,6 +10,7 @@ import ( "github.com/wandb/operator/pkg/utils" chkv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1" chiv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + "github.com/wandb/operator/pkg/wandb/manifest" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,6 +20,14 @@ import ( ctrl "sigs.k8s.io/controller-runtime" ) +func KeeperImage(img manifest.ImageRef, globalImageRegistry string) string { + if out := img.GetImage(globalImageRegistry); out != "" { + return out + } + // Fallback for older manifests that don't supply the image. + return defaultKeeperImage +} + // ToKeeperVendorSpec builds the ClickHouseKeeperInstallation CR that coordinates // ReplicatedMergeTree replication. nsName comes from altinity.KeeperNsName — // this package never sees the "-chi"-suffixed spec name. @@ -28,6 +37,7 @@ func ToKeeperVendorSpec( spec *apiv2.ManagedClickHouseSpec, scheme *runtime.Scheme, nsName types.NamespacedName, + mfst manifest.Manifest, ) (*chkv1.ClickHouseKeeperInstallation, error) { _, log := logx.WithSlog(ctx, logx.ClickHouse) if spec == nil { @@ -50,7 +60,7 @@ func ToKeeperVendorSpec( Containers: []corev1.Container{ { Name: keeperContainerName, - Image: KeeperImage, + Image: KeeperImage(mfst.ClickhouseKeeper["default"].Images["keeper"], wandb.Spec.Global.ImageRegistry), SecurityContext: keeperContainerSecurityContext(), }, }, diff --git a/internal/controller/infra/managed/clickhouse/altinity/keeper/spec_test.go b/internal/controller/infra/managed/clickhouse/altinity/keeper/spec_test.go index 78319ff4..1376cdc7 100644 --- a/internal/controller/infra/managed/clickhouse/altinity/keeper/spec_test.go +++ b/internal/controller/infra/managed/clickhouse/altinity/keeper/spec_test.go @@ -9,6 +9,7 @@ import ( "github.com/wandb/operator/pkg/utils" chkv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse-keeper.altinity.com/v1" chiv1 "github.com/wandb/operator/pkg/vendored/altinity-clickhouse/clickhouse.altinity.com/v1" + "github.com/wandb/operator/pkg/wandb/manifest" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -33,7 +34,7 @@ var _ = Describe("Keeper vendor spec", func() { }, } - chk, err := ToKeeperVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, keeperScheme(), keeperNsName()) + chk, err := ToKeeperVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, keeperScheme(), keeperNsName(), manifest.Manifest{}) Expect(err).NotTo(HaveOccurred()) Expect(chk).NotTo(BeNil()) Expect(chk.Name).To(Equal("clickhouse-chk")) @@ -48,7 +49,7 @@ var _ = Describe("Keeper vendor spec", func() { Expect(chk.Spec.Templates.PodTemplates).To(HaveLen(1)) container := chk.Spec.Templates.PodTemplates[0].Spec.Containers[0] - Expect(container.Image).To(Equal(KeeperImage)) + Expect(container.Image).To(Equal(KeeperImage(manifest.ImageRef{}, ""))) Expect(container.Name).To(Equal(keeperContainerName)) Expect(container.Resources.Requests[corev1.ResourceCPU]).To(Equal(resource.MustParse("250m"))) @@ -60,17 +61,36 @@ var _ = Describe("Keeper vendor spec", func() { Expect(*sc.RunAsNonRoot).To(BeTrue()) }) + It("uses the Keeper image from the server manifest", func() { + wandb := keeperWandb() + wandb.Spec.Global.ImageRegistry = "myregistry.io" + mfst := manifest.Manifest{ + ClickhouseKeeper: map[string]manifest.InfraConfig{ + "default": { + Images: map[string]manifest.ImageRef{ + "keeper": {Registry: "docker.io", Repository: "altinity/clickhouse-keeper", Tag: "25.8"}, + }, + }, + }, + } + + chk, err := ToKeeperVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, keeperScheme(), keeperNsName(), mfst) + Expect(err).NotTo(HaveOccurred()) + Expect(chk.Spec.Templates.PodTemplates[0].Spec.Containers[0].Image). + To(Equal("myregistry.io/docker.io/altinity/clickhouse-keeper:25.8")) + }) + It("errors when keeper storage size is unset (no operator defaults)", func() { wandb := keeperWandb() wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse.Keeper = apiv2.ClickHouseKeeperSpec{} - _, err := ToKeeperVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, keeperScheme(), keeperNsName()) + _, err := ToKeeperVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, keeperScheme(), keeperNsName(), manifest.Manifest{}) Expect(err).To(HaveOccurred()) }) It("omits fixed IDs in OpenShift mode", func() { utils.SetOpenShiftMode(true) wandb := keeperWandb() - chk, err := ToKeeperVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, keeperScheme(), keeperNsName()) + chk, err := ToKeeperVendorSpec(context.Background(), wandb, wandb.Spec.ClickHouse[apiv2.DefaultInstanceName].ManagedClickHouse, keeperScheme(), keeperNsName(), manifest.Manifest{}) Expect(err).NotTo(HaveOccurred()) sc := chk.Spec.Templates.PodTemplates[0].Spec.SecurityContext Expect(sc.RunAsUser).To(BeNil()) diff --git a/internal/controller/infra/managed/clickhouse/altinity/keeper/values.go b/internal/controller/infra/managed/clickhouse/altinity/keeper/values.go index c5bc4f6f..df41cb37 100644 --- a/internal/controller/infra/managed/clickhouse/altinity/keeper/values.go +++ b/internal/controller/infra/managed/clickhouse/altinity/keeper/values.go @@ -4,8 +4,10 @@ const ( // KeeperModuleName is the W&B component label value for Keeper resources. KeeperModuleName = "clickhouse-keeper" - // KeeperImage pins the Keeper image to the managed ClickHouse server version. - KeeperImage = "altinity/clickhouse-keeper:25.8.16.10002.altinitystable" + // TODO: remove this hardcoded default once all supported manifest versions + // supply clickhouseKeeper.<instance>.images.keeper. Pinned to the managed + // ClickHouse server version. + defaultKeeperImage = "altinity/clickhouse-keeper:25.8.16.10002.altinitystable" // KeeperClientPort is the ZooKeeper-compatible client port. KeeperClientPort = 2181 diff --git a/internal/controller/reconciler/clickhouse.go b/internal/controller/reconciler/clickhouse.go index f842da17..ed98ba24 100644 --- a/internal/controller/reconciler/clickhouse.go +++ b/internal/controller/reconciler/clickhouse.go @@ -198,7 +198,7 @@ func managedClickHouseWriteState( } // Translate the Keeper and ClickHouse CRs; WriteState writes Keeper first. - desiredKeeper, err := keeper.ToKeeperVendorSpec(ctx, wandb, spec, client.Scheme(), altinity.KeeperNsName(spec)) + desiredKeeper, err := keeper.ToKeeperVendorSpec(ctx, wandb, spec, client.Scheme(), altinity.KeeperNsName(spec), mfst) if err != nil { log.Error(err, "failed to translate Keeper spec to vendor spec") return []metav1.Condition{